@xsynaptic/sonic-ui 0.6.1 → 0.9.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/README.md +2 -0
- package/dist/default/define/spectrum.js +7 -0
- package/dist/default/define.js +1 -0
- package/dist/default/elements/button.js +30 -2
- package/dist/default/elements/dial.js +12 -3
- package/dist/default/elements/display-element.js +77 -0
- package/dist/default/elements/envelope.js +3 -3
- package/dist/default/elements/modulation.js +45 -0
- package/dist/default/elements/readout-claim.js +8 -3
- package/dist/default/elements/slider.js +68 -4
- package/dist/default/elements/spectrum.js +230 -0
- package/dist/default/elements/value-element.js +114 -317
- package/dist/default/elements/value-gestures.js +301 -0
- package/dist/default/elements/wave-element.js +59 -9
- package/dist/default/elements/waveform.js +61 -12
- package/dist/default/elements/wavestrip.js +25 -49
- package/dist/default/elements/xy.js +10 -21
- package/dist/default/index.js +2 -1
- package/dist/default/lib/asked-value.js +21 -0
- package/dist/default/lib/canvas-surface.js +10 -4
- package/dist/default/lib/frame-timeline.js +2 -2
- package/dist/default/lib/hover-preview.js +46 -0
- package/dist/default/lib/marker-rider.js +12 -6
- package/dist/default/lib/pointer-drag.js +7 -2
- package/dist/default/lib/read-ms-property.js +10 -0
- package/dist/default/lib/spectrum-columns.js +74 -0
- package/dist/default/lib/spectrum-levels.js +55 -0
- package/dist/default/lib/strip-scene.js +12 -1
- package/dist/default/lib/time-regions.js +5 -1
- package/dist/dev/define/spectrum.d.ts +2 -0
- package/dist/dev/define/spectrum.js +8 -0
- package/dist/dev/define/spectrum.js.map +1 -0
- package/dist/dev/define.d.ts +3 -1
- package/dist/dev/define.js +1 -0
- package/dist/dev/elements/button.d.ts +2 -0
- package/dist/dev/elements/button.js +30 -2
- package/dist/dev/elements/button.js.map +1 -1
- package/dist/dev/elements/dial.js +12 -3
- package/dist/dev/elements/dial.js.map +1 -1
- package/dist/dev/elements/display-element.d.ts +29 -0
- package/dist/dev/elements/display-element.js +78 -0
- package/dist/dev/elements/display-element.js.map +1 -0
- package/dist/dev/elements/envelope.js +3 -3
- package/dist/dev/elements/envelope.js.map +1 -1
- package/dist/dev/elements/modulation.js +46 -0
- package/dist/dev/elements/modulation.js.map +1 -0
- package/dist/dev/elements/readout-claim.js +8 -3
- package/dist/dev/elements/readout-claim.js.map +1 -1
- package/dist/dev/elements/slider.d.ts +5 -0
- package/dist/dev/elements/slider.js +68 -4
- package/dist/dev/elements/slider.js.map +1 -1
- package/dist/dev/elements/spectrum.d.ts +59 -0
- package/dist/dev/elements/spectrum.js +231 -0
- package/dist/dev/elements/spectrum.js.map +1 -0
- package/dist/dev/elements/value-element.d.ts +13 -14
- package/dist/dev/elements/value-element.js +114 -317
- package/dist/dev/elements/value-element.js.map +1 -1
- package/dist/dev/elements/value-gestures.d.ts +10 -0
- package/dist/dev/elements/value-gestures.js +302 -0
- package/dist/dev/elements/value-gestures.js.map +1 -0
- package/dist/dev/elements/wave-element.d.ts +8 -2
- package/dist/dev/elements/wave-element.js +59 -9
- package/dist/dev/elements/wave-element.js.map +1 -1
- package/dist/dev/elements/waveform.d.ts +12 -4
- package/dist/dev/elements/waveform.js +61 -12
- package/dist/dev/elements/waveform.js.map +1 -1
- package/dist/dev/elements/wavestrip.d.ts +3 -1
- package/dist/dev/elements/wavestrip.js +25 -49
- package/dist/dev/elements/wavestrip.js.map +1 -1
- package/dist/dev/elements/xy.js +10 -21
- package/dist/dev/elements/xy.js.map +1 -1
- package/dist/dev/index.d.ts +4 -1
- package/dist/dev/index.js +2 -1
- package/dist/dev/lib/asked-value.js +22 -0
- package/dist/dev/lib/asked-value.js.map +1 -0
- package/dist/dev/lib/canvas-surface.d.ts +5 -3
- package/dist/dev/lib/canvas-surface.js +10 -4
- package/dist/dev/lib/canvas-surface.js.map +1 -1
- package/dist/dev/lib/frame-timeline.js +2 -2
- package/dist/dev/lib/frame-timeline.js.map +1 -1
- package/dist/dev/lib/hover-preview.js +47 -0
- package/dist/dev/lib/hover-preview.js.map +1 -0
- package/dist/dev/lib/marker-rider.js +12 -6
- package/dist/dev/lib/marker-rider.js.map +1 -1
- package/dist/dev/lib/pointer-drag.js +7 -2
- package/dist/dev/lib/pointer-drag.js.map +1 -1
- package/dist/dev/lib/read-ms-property.js +11 -0
- package/dist/dev/lib/read-ms-property.js.map +1 -0
- package/dist/dev/lib/spectrum-columns.js +75 -0
- package/dist/dev/lib/spectrum-columns.js.map +1 -0
- package/dist/dev/lib/spectrum-levels.js +56 -0
- package/dist/dev/lib/spectrum-levels.js.map +1 -0
- package/dist/dev/lib/strip-scene.js +12 -1
- package/dist/dev/lib/strip-scene.js.map +1 -1
- package/dist/dev/lib/time-regions.js +5 -1
- package/dist/dev/lib/time-regions.js.map +1 -1
- package/dist/styles/button.css +7 -1
- package/dist/styles/controls.css +1 -0
- package/dist/styles/material.css +36 -3
- package/dist/styles/meter.css +22 -18
- package/dist/styles/slider.css +42 -5
- package/dist/styles/spectrum.css +105 -0
- package/dist/styles/waveform.css +48 -16
- package/dist/styles/wavestrip.css +26 -5
- package/package.json +2 -1
|
@@ -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<Colour extends string, Length extends string = never> {\n\tcolours: Record<Colour, string>;\n\tisReducedMotion: boolean;\n\tlengths: Record<Length, number>;\n}\n\nexport interface SurfaceFrame<Colour extends string, Length extends string = never> {\n\tframeMs: number;\n\tisDirty: boolean;\n\tisRebuilt: boolean;\n\tlook: SurfaceLook<Colour, Length>;\n\tsize: SurfaceSize;\n}\n\nexport interface Surface {\n\tinvalidate: () => void;\n\trebuild: () => void;\n\trequestFrame: () => void;\n\treadonly size: SurfaceSize;\n}\n\ninterface SurfaceOptions<Colour extends string, Length extends string> {\n\tcanvas: HTMLCanvasElement;\n\tcolours: Record<Colour, PrivateProperty>;\n\tlengths?: Record<Length, PrivateProperty>;\n\tpaint: (context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length>) => 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<Colour extends string, Length extends string> implements Surface {\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> | undefined;\n\n\treadonly #options: SurfaceOptions<Colour, Length>;\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>) {\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 { style } = this.#options.canvas;\n\n\t\tstyle.setProperty('transition-property', Object.values(this.#options.colours).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> } {\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> {\n\t\tconst { canvas, colours, lengths = {} } = 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};\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<Colour extends string, Length extends string = never>(\n\toptions: SurfaceOptions<Colour, Length>,\n): 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\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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"}
|
|
@@ -20,8 +20,8 @@ function pendingIn({ pending, range }, window) {
|
|
|
20
20
|
return shown ? [shown] : [];
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
function placeholderPhase({ frameMs,
|
|
24
|
-
return !
|
|
23
|
+
function placeholderPhase({ frameMs, isStill }, pending) {
|
|
24
|
+
return !isStill && pending.length > 0 ? frameMs / placeholderPeriodMs : void 0;
|
|
25
25
|
}
|
|
26
26
|
function frameTimeline(input) {
|
|
27
27
|
const { clockSeconds, held, range, size } = input;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frame-timeline.js","names":[],"sources":["../../../src/lib/frame-timeline.ts"],"sourcesContent":["import type { SurfaceSize } from '#lib/canvas-surface.ts';\n\nimport { trimFloat } from '#lib/math.ts';\n\nexport interface View {\n\tdpr: number;\n\theight: number;\n\tpending: Array<[number, number]>;\n\tphase: number;\n\tpixelsPerSecond: number;\n\tstartSeconds: number;\n\twidth: number;\n}\n\nexport interface Drawn {\n\tseconds: number;\n\tstartSeconds: number;\n}\n\ninterface TimelineInput {\n\tclockSeconds: number;\n\tframeMs: number;\n\theld?: { grabbed: Drawn; seconds: number };\n\tisPaged: boolean;\n\tisPlaying: boolean;\n\tpending: ReadonlyArray<[number, number]>;\n\trange: [number, number];\n\tsize: SurfaceSize;\n\tzoom: number;\n}\n\ninterface Timeline {\n\tghostAt?: number;\n\tisMoving: boolean;\n\tpaintKey: string;\n\tplayheadAt: number;\n\tseconds: number;\n\tview: View;\n\twanted?: [number, number];\n}\n\nconst placeholderPeriodMs = 2500;\n\nconst lookAheadSeconds = 30;\n\nfunction windowStart(input: TimelineInput, seconds: number, windowSeconds: number): number {\n\tif (!input.isPaged) return seconds - windowSeconds / 2;\n\tif (input.held) return input.held.grabbed.startSeconds + seconds - input.held.grabbed.seconds;\n\n\t// 0.3 / 0.1 is 2.9999999999999996, a page short\n\treturn Math.floor(trimFloat(seconds / windowSeconds)) * windowSeconds;\n}\n\nfunction overlap(\n\t[from, to]: readonly [number, number],\n\t[low, high]: readonly [number, number],\n): [number, number] | undefined {\n\tconst start = Math.max(from, low);\n\tconst end = Math.min(to, high);\n\n\treturn end > start ? [start, end] : undefined;\n}\n\nfunction pendingIn(\n\t{ pending, range }: TimelineInput,\n\twindow: [number, number],\n): Array<[number, number]> {\n\treturn pending.flatMap((region) => {\n\t\tconst inRange = overlap(region, range);\n\t\tconst shown = inRange && overlap(inRange, window);\n\n\t\treturn shown ? [shown] : [];\n\t});\n}\n\nfunction placeholderPhase(\n\t{ frameMs,
|
|
1
|
+
{"version":3,"file":"frame-timeline.js","names":[],"sources":["../../../src/lib/frame-timeline.ts"],"sourcesContent":["import type { SurfaceSize } from '#lib/canvas-surface.ts';\n\nimport { trimFloat } from '#lib/math.ts';\n\nexport interface View {\n\tdpr: number;\n\theight: number;\n\tpending: Array<[number, number]>;\n\tphase: number;\n\tpixelsPerSecond: number;\n\tstartSeconds: number;\n\twidth: number;\n}\n\nexport interface Drawn {\n\tseconds: number;\n\tstartSeconds: number;\n}\n\ninterface TimelineInput {\n\tclockSeconds: number;\n\tframeMs: number;\n\theld?: { grabbed: Drawn; seconds: number };\n\tisPaged: boolean;\n\tisPlaying: boolean;\n\tisStill: boolean;\n\tpending: ReadonlyArray<[number, number]>;\n\trange: [number, number];\n\tsize: SurfaceSize;\n\tzoom: number;\n}\n\ninterface Timeline {\n\tghostAt?: number;\n\tisMoving: boolean;\n\tpaintKey: string;\n\tplayheadAt: number;\n\tseconds: number;\n\tview: View;\n\twanted?: [number, number];\n}\n\nconst placeholderPeriodMs = 2500;\n\nconst lookAheadSeconds = 30;\n\nfunction windowStart(input: TimelineInput, seconds: number, windowSeconds: number): number {\n\tif (!input.isPaged) return seconds - windowSeconds / 2;\n\tif (input.held) return input.held.grabbed.startSeconds + seconds - input.held.grabbed.seconds;\n\n\t// 0.3 / 0.1 is 2.9999999999999996, a page short\n\treturn Math.floor(trimFloat(seconds / windowSeconds)) * windowSeconds;\n}\n\nfunction overlap(\n\t[from, to]: readonly [number, number],\n\t[low, high]: readonly [number, number],\n): [number, number] | undefined {\n\tconst start = Math.max(from, low);\n\tconst end = Math.min(to, high);\n\n\treturn end > start ? [start, end] : undefined;\n}\n\nfunction pendingIn(\n\t{ pending, range }: TimelineInput,\n\twindow: [number, number],\n): Array<[number, number]> {\n\treturn pending.flatMap((region) => {\n\t\tconst inRange = overlap(region, range);\n\t\tconst shown = inRange && overlap(inRange, window);\n\n\t\treturn shown ? [shown] : [];\n\t});\n}\n\nfunction placeholderPhase(\n\t{ frameMs, isStill }: TimelineInput,\n\tpending: ReadonlyArray<unknown>,\n): number | undefined {\n\treturn !isStill && pending.length > 0 ? frameMs / placeholderPeriodMs : undefined;\n}\n\nexport function frameTimeline(input: TimelineInput): Timeline {\n\tconst { clockSeconds, held, range, size } = input;\n\tconst seconds = held?.seconds ?? clockSeconds;\n\tconst pixelsPerSecond = input.zoom * size.dpr;\n\tconst windowSeconds = size.width / pixelsPerSecond;\n\tconst startSeconds = windowStart(input, seconds, windowSeconds);\n\tconst pending = pendingIn(input, [startSeconds, startSeconds + windowSeconds]);\n\tconst placeholder = placeholderPhase(input, pending);\n\tconst phase = placeholder ?? 0;\n\tconst wanted = overlap(\n\t\t[\n\t\t\tstartSeconds - windowSeconds,\n\t\t\tstartSeconds + windowSeconds + Math.max(windowSeconds, lookAheadSeconds),\n\t\t],\n\t\trange,\n\t);\n\tconst at = (time: number): number => (time - startSeconds) / windowSeconds;\n\n\treturn {\n\t\t...(held ? { ghostAt: at(clockSeconds) } : {}),\n\t\tisMoving: held !== undefined || placeholder !== undefined || input.isPlaying,\n\t\tpaintKey: [startSeconds, windowSeconds, ...range, phase, pending.join(',')].join(':'),\n\t\tplayheadAt: at(seconds),\n\t\tseconds,\n\t\tview: {\n\t\t\tdpr: size.dpr,\n\t\t\theight: size.height,\n\t\t\tpending,\n\t\t\tphase,\n\t\t\tpixelsPerSecond,\n\t\t\tstartSeconds,\n\t\t\twidth: size.width,\n\t\t},\n\t\t...(wanted ? { wanted } : {}),\n\t};\n}\n"],"mappings":";;;AA0CA,MAAM,sBAAsB;AAE5B,MAAM,mBAAmB;AAEzB,SAAS,YAAY,OAAsB,SAAiB,eAA+B;CAC1F,IAAI,CAAC,MAAM,SAAS,OAAO,UAAU,gBAAgB;CACrD,IAAI,MAAM,MAAM,OAAO,MAAM,KAAK,QAAQ,eAAe,UAAU,MAAM,KAAK,QAAQ;CAGtF,OAAO,KAAK,MAAM,UAAU,UAAU,aAAa,CAAC,IAAI;AACzD;AAEA,SAAS,QACR,CAAC,MAAM,KACP,CAAC,KAAK,OACyB;CAC/B,MAAM,QAAQ,KAAK,IAAI,MAAM,GAAG;CAChC,MAAM,MAAM,KAAK,IAAI,IAAI,IAAI;CAE7B,OAAO,MAAM,QAAQ,CAAC,OAAO,GAAG,IAAI;AACrC;AAEA,SAAS,UACR,EAAE,SAAS,SACX,QAC0B;CAC1B,OAAO,QAAQ,SAAS,WAAW;EAClC,MAAM,UAAU,QAAQ,QAAQ,KAAK;EACrC,MAAM,QAAQ,WAAW,QAAQ,SAAS,MAAM;EAEhD,OAAO,QAAQ,CAAC,KAAK,IAAI,CAAC;CAC3B,CAAC;AACF;AAEA,SAAS,iBACR,EAAE,SAAS,WACX,SACqB;CACrB,OAAO,CAAC,WAAW,QAAQ,SAAS,IAAI,UAAU,sBAAsB;AACzE;AAEA,SAAgB,cAAc,OAAgC;CAC7D,MAAM,EAAE,cAAc,MAAM,OAAO,SAAS;CAC5C,MAAM,UAAU,MAAM,WAAW;CACjC,MAAM,kBAAkB,MAAM,OAAO,KAAK;CAC1C,MAAM,gBAAgB,KAAK,QAAQ;CACnC,MAAM,eAAe,YAAY,OAAO,SAAS,aAAa;CAC9D,MAAM,UAAU,UAAU,OAAO,CAAC,cAAc,eAAe,aAAa,CAAC;CAC7E,MAAM,cAAc,iBAAiB,OAAO,OAAO;CACnD,MAAM,QAAQ,eAAe;CAC7B,MAAM,SAAS,QACd,CACC,eAAe,eACf,eAAe,gBAAgB,KAAK,IAAI,eAAe,gBAAgB,CACxE,GACA,KACD;CACA,MAAM,MAAM,UAA0B,OAAO,gBAAgB;CAE7D,OAAO;EACN,GAAI,OAAO,EAAE,SAAS,GAAG,YAAY,EAAE,IAAI,CAAC;EAC5C,UAAU,SAAS,UAAa,gBAAgB,UAAa,MAAM;EACnE,UAAU;GAAC;GAAc;GAAe,GAAG;GAAO;GAAO,QAAQ,KAAK,GAAG;EAAC,CAAC,CAAC,KAAK,GAAG;EACpF,YAAY,GAAG,OAAO;EACtB;EACA,MAAM;GACL,KAAK,KAAK;GACV,QAAQ,KAAK;GACb;GACA;GACA;GACA;GACA,OAAO,KAAK;EACb;EACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;CAC5B;AACD"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/lib/hover-preview.ts
|
|
2
|
+
function bindHoverPreview(target, options, signal) {
|
|
3
|
+
let hover;
|
|
4
|
+
let isDismissed = false;
|
|
5
|
+
let escapeWatch;
|
|
6
|
+
const watchEscape = (isHovered) => {
|
|
7
|
+
if (!isHovered) {
|
|
8
|
+
escapeWatch?.abort();
|
|
9
|
+
escapeWatch = void 0;
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (escapeWatch) return;
|
|
13
|
+
escapeWatch = new AbortController();
|
|
14
|
+
target.ownerDocument.addEventListener("keydown", (event) => {
|
|
15
|
+
if (hover === void 0 || event.key !== "Escape") return;
|
|
16
|
+
hover = void 0;
|
|
17
|
+
isDismissed = true;
|
|
18
|
+
options.dismiss();
|
|
19
|
+
}, { signal: escapeWatch.signal });
|
|
20
|
+
};
|
|
21
|
+
const hoverAt = (value) => {
|
|
22
|
+
if (value === void 0) isDismissed = false;
|
|
23
|
+
if (!isDismissed && !options.isTaken()) hover = value;
|
|
24
|
+
options.show();
|
|
25
|
+
watchEscape(value !== void 0);
|
|
26
|
+
};
|
|
27
|
+
target.addEventListener("pointermove", (event) => {
|
|
28
|
+
if (event.pointerType === "touch" || event.buttons !== 0 || !options.canShow()) return;
|
|
29
|
+
hoverAt(options.valueAt(event));
|
|
30
|
+
}, { signal });
|
|
31
|
+
for (const type of ["pointerdown", "pointerleave"]) target.addEventListener(type, () => {
|
|
32
|
+
hoverAt(void 0);
|
|
33
|
+
}, { signal });
|
|
34
|
+
signal.addEventListener("abort", () => {
|
|
35
|
+
watchEscape(false);
|
|
36
|
+
}, { once: true });
|
|
37
|
+
return {
|
|
38
|
+
clear: () => {
|
|
39
|
+
hover = void 0;
|
|
40
|
+
},
|
|
41
|
+
value: () => hover
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { bindHoverPreview };
|
|
47
|
+
//# sourceMappingURL=hover-preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hover-preview.js","names":[],"sources":["../../../src/lib/hover-preview.ts"],"sourcesContent":["interface HoverOptions {\n\tcanShow: () => boolean;\n\tdismiss: () => void;\n\tisTaken: () => boolean;\n\tshow: () => void;\n\tvalueAt: (event: PointerEvent) => number | undefined;\n}\n\nexport interface HoverPreview {\n\tclear: () => void;\n\tvalue: () => number | undefined;\n}\n\nexport function bindHoverPreview(\n\ttarget: HTMLElement,\n\toptions: HoverOptions,\n\tsignal: AbortSignal,\n): HoverPreview {\n\tlet hover: number | undefined;\n\tlet isDismissed = false;\n\tlet escapeWatch: AbortController | undefined;\n\n\t// A hover holds no focus, so the key is heard on the document (WCAG 1.4.13)\n\tconst watchEscape = (isHovered: boolean): void => {\n\t\tif (!isHovered) {\n\t\t\tescapeWatch?.abort();\n\t\t\tescapeWatch = undefined;\n\t\t\treturn;\n\t\t}\n\t\tif (escapeWatch) return;\n\n\t\tescapeWatch = new AbortController();\n\t\ttarget.ownerDocument.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (hover === undefined || event.key !== 'Escape') return;\n\n\t\t\t\t// Stays dismissed until the hover ends, or the next pointer move would bring it straight back\n\t\t\t\thover = undefined;\n\t\t\t\tisDismissed = true;\n\t\t\t\toptions.dismiss();\n\t\t\t},\n\t\t\t{ signal: escapeWatch.signal },\n\t\t);\n\t};\n\tconst hoverAt = (value: number | undefined): void => {\n\t\tif (value === undefined) isDismissed = false;\n\t\tif (!isDismissed && !options.isTaken()) hover = value;\n\t\toptions.show();\n\t\twatchEscape(value !== undefined);\n\t};\n\n\ttarget.addEventListener(\n\t\t'pointermove',\n\t\t(event) => {\n\t\t\tif (event.pointerType === 'touch' || event.buttons !== 0 || !options.canShow()) return;\n\n\t\t\thoverAt(options.valueAt(event));\n\t\t},\n\t\t{ signal },\n\t);\n\tfor (const type of ['pointerdown', 'pointerleave']) {\n\t\ttarget.addEventListener(\n\t\t\ttype,\n\t\t\t() => {\n\t\t\t\thoverAt(undefined);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\tsignal.addEventListener(\n\t\t'abort',\n\t\t() => {\n\t\t\twatchEscape(false);\n\t\t},\n\t\t{ once: true },\n\t);\n\n\treturn {\n\t\tclear: () => {\n\t\t\thover = undefined;\n\t\t},\n\t\tvalue: () => hover,\n\t};\n}\n"],"mappings":";AAaA,SAAgB,iBACf,QACA,SACA,QACe;CACf,IAAI;CACJ,IAAI,cAAc;CAClB,IAAI;CAGJ,MAAM,eAAe,cAA6B;EACjD,IAAI,CAAC,WAAW;GACf,aAAa,MAAM;GACnB,cAAc;GACd;EACD;EACA,IAAI,aAAa;EAEjB,cAAc,IAAI,gBAAgB;EAClC,OAAO,cAAc,iBACpB,YACC,UAAU;GACV,IAAI,UAAU,UAAa,MAAM,QAAQ,UAAU;GAGnD,QAAQ;GACR,cAAc;GACd,QAAQ,QAAQ;EACjB,GACA,EAAE,QAAQ,YAAY,OAAO,CAC9B;CACD;CACA,MAAM,WAAW,UAAoC;EACpD,IAAI,UAAU,QAAW,cAAc;EACvC,IAAI,CAAC,eAAe,CAAC,QAAQ,QAAQ,GAAG,QAAQ;EAChD,QAAQ,KAAK;EACb,YAAY,UAAU,MAAS;CAChC;CAEA,OAAO,iBACN,gBACC,UAAU;EACV,IAAI,MAAM,gBAAgB,WAAW,MAAM,YAAY,KAAK,CAAC,QAAQ,QAAQ,GAAG;EAEhF,QAAQ,QAAQ,QAAQ,KAAK,CAAC;CAC/B,GACA,EAAE,OAAO,CACV;CACA,KAAK,MAAM,QAAQ,CAAC,eAAe,cAAc,GAChD,OAAO,iBACN,YACM;EACL,QAAQ,MAAS;CAClB,GACA,EAAE,OAAO,CACV;CAED,OAAO,iBACN,eACM;EACL,YAAY,KAAK;CAClB,GACA,EAAE,MAAM,KAAK,CACd;CAEA,OAAO;EACN,aAAa;GACZ,QAAQ;EACT;EACA,aAAa;CACd;AACD"}
|
|
@@ -4,7 +4,6 @@ import { readPxProperty } from "./read-px-property.js";
|
|
|
4
4
|
import { writeKind } from "./marker-band.js";
|
|
5
5
|
|
|
6
6
|
//#region src/lib/marker-rider.ts
|
|
7
|
-
const fadeShare = .25;
|
|
8
7
|
function lineX(seconds, view) {
|
|
9
8
|
return (seconds - view.startSeconds) / view.windowSeconds * view.widthPx;
|
|
10
9
|
}
|
|
@@ -12,11 +11,11 @@ function parkedPlacement(labels, index, [view, arrivingX]) {
|
|
|
12
11
|
const parked = labels[index];
|
|
13
12
|
if (!parked) return void 0;
|
|
14
13
|
const next = labels[index + 1];
|
|
15
|
-
const x = Math.max(view.
|
|
14
|
+
const x = Math.max(view.parkPx, lineX(parked.start, view) + view.insetPx);
|
|
16
15
|
const approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;
|
|
17
16
|
return {
|
|
18
17
|
index,
|
|
19
|
-
opacity: clamp(approachPx / (view.widthPx * fadeShare), 0, 1),
|
|
18
|
+
opacity: view.fadeShare > 0 ? clamp(approachPx / (view.widthPx * view.fadeShare), 0, 1) : 1,
|
|
20
19
|
shownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),
|
|
21
20
|
x
|
|
22
21
|
};
|
|
@@ -91,15 +90,20 @@ function measureWidths(control, className, texts) {
|
|
|
91
90
|
return widths;
|
|
92
91
|
}
|
|
93
92
|
function createLabelRider(control, options) {
|
|
94
|
-
const { className, colourProperty, insetProperty } = options;
|
|
93
|
+
const { className, colourProperty, fadeProperty, insetProperty, parkProperty } = options;
|
|
95
94
|
const parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));
|
|
96
95
|
const arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));
|
|
96
|
+
let fadeShare = .25;
|
|
97
97
|
let insetPx = 0;
|
|
98
|
+
let parkPx = 0;
|
|
98
99
|
let measured = [];
|
|
99
100
|
return {
|
|
100
101
|
measure: (labels) => {
|
|
101
102
|
const widths = measureWidths(control, className, labels.map(({ text }) => text));
|
|
102
|
-
|
|
103
|
+
const styles = getComputedStyle(control);
|
|
104
|
+
fadeShare = readPxProperty(styles, fadeProperty, .25);
|
|
105
|
+
insetPx = readPxProperty(styles, insetProperty, 0);
|
|
106
|
+
parkPx = readPxProperty(styles, parkProperty, 0);
|
|
103
107
|
measured = labels.map((label, index) => ({
|
|
104
108
|
...label,
|
|
105
109
|
widthPx: widths[index] ?? 0
|
|
@@ -108,7 +112,9 @@ function createLabelRider(control, options) {
|
|
|
108
112
|
place: (window) => {
|
|
109
113
|
const layout = layoutRider(measured, {
|
|
110
114
|
...window,
|
|
111
|
-
|
|
115
|
+
fadeShare,
|
|
116
|
+
insetPx,
|
|
117
|
+
parkPx
|
|
112
118
|
});
|
|
113
119
|
writeLabel(parked, [measured, colourProperty], layout.parked);
|
|
114
120
|
writeLabel(arriving, [measured, colourProperty], layout.arriving);
|
|
@@ -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\tinsetPx: 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\
|
|
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,4 +1,9 @@
|
|
|
1
1
|
//#region src/lib/pointer-drag.ts
|
|
2
|
+
function capturePointer(target, pointerId) {
|
|
3
|
+
try {
|
|
4
|
+
target.setPointerCapture(pointerId);
|
|
5
|
+
} catch {}
|
|
6
|
+
}
|
|
2
7
|
function bindDrag(target, handlers, signal) {
|
|
3
8
|
let held;
|
|
4
9
|
const end = () => {
|
|
@@ -22,8 +27,8 @@ function bindDrag(target, handlers, signal) {
|
|
|
22
27
|
drag,
|
|
23
28
|
pointerId: event.pointerId
|
|
24
29
|
};
|
|
25
|
-
target.setPointerCapture(event.pointerId);
|
|
26
30
|
handlers.toggle?.(true);
|
|
31
|
+
capturePointer(target, event.pointerId);
|
|
27
32
|
}, { signal });
|
|
28
33
|
whileHeld("pointermove", handlers.move);
|
|
29
34
|
whileHeld("pointercancel", (drag) => {
|
|
@@ -43,5 +48,5 @@ function bindDrag(target, handlers, signal) {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
//#endregion
|
|
46
|
-
export { bindDrag };
|
|
51
|
+
export { bindDrag, capturePointer };
|
|
47
52
|
//# sourceMappingURL=pointer-drag.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pointer-drag.js","names":[],"sources":["../../../src/lib/pointer-drag.ts"],"sourcesContent":["interface DragHandlers<Drag> {\n\tcancel?: (drag: Drag) => void;\n\tgrab: (event: PointerEvent) => Drag | undefined;\n\tlift?: (drag: Drag, event: PointerEvent) => void;\n\tmove: (drag: Drag, event: PointerEvent) => void;\n\trelease?: (drag: Drag) => void;\n\ttoggle?: (isDragging: boolean) => void;\n}\n\nexport interface PointerDrag<Drag> {\n\tcurrent: () => Drag | undefined;\n\tend: () => void;\n}\n\nexport function bindDrag<Drag>(\n\ttarget: HTMLElement,\n\thandlers: DragHandlers<Drag>,\n\tsignal: AbortSignal,\n): PointerDrag<Drag> {\n\tlet held: undefined | { drag: Drag; pointerId: number };\n\n\tconst end = (): void => {\n\t\tif (!held) return;\n\n\t\tconst { drag } = held;\n\n\t\theld = undefined;\n\t\thandlers.toggle?.(false);\n\t\thandlers.release?.(drag);\n\t};\n\tconst whileHeld = (\n\t\ttype: 'lostpointercapture' | 'pointercancel' | 'pointermove' | 'pointerup',\n\t\thandle: (drag: Drag, event: PointerEvent) => void,\n\t): void => {\n\t\ttarget.addEventListener(\n\t\t\ttype,\n\t\t\t(event) => {\n\t\t\t\tif (held?.pointerId === event.pointerId) handle(held.drag, event);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t};\n\n\ttarget.addEventListener(\n\t\t'pointerdown',\n\t\t(event) => {\n\t\t\tif (event.button !== 0 || (held && held.pointerId !== event.pointerId)) return;\n\n\t\t\tend();\n\n\t\t\tconst drag = handlers.grab(event);\n\t\t\tif (drag === undefined) return;\n\n\t\t\theld = { drag, pointerId: event.pointerId };\n\t\t\
|
|
1
|
+
{"version":3,"file":"pointer-drag.js","names":[],"sources":["../../../src/lib/pointer-drag.ts"],"sourcesContent":["interface DragHandlers<Drag> {\n\tcancel?: (drag: Drag) => void;\n\tgrab: (event: PointerEvent) => Drag | undefined;\n\tlift?: (drag: Drag, event: PointerEvent) => void;\n\tmove: (drag: Drag, event: PointerEvent) => void;\n\trelease?: (drag: Drag) => void;\n\ttoggle?: (isDragging: boolean) => void;\n}\n\nexport interface PointerDrag<Drag> {\n\tcurrent: () => Drag | undefined;\n\tend: () => void;\n}\n\nexport function capturePointer(target: HTMLElement, pointerId: number): void {\n\ttry {\n\t\ttarget.setPointerCapture(pointerId);\n\t} catch {\n\t\t// A synthetic pointer has no id to capture; the gesture carries on uncaptured\n\t}\n}\n\nexport function bindDrag<Drag>(\n\ttarget: HTMLElement,\n\thandlers: DragHandlers<Drag>,\n\tsignal: AbortSignal,\n): PointerDrag<Drag> {\n\tlet held: undefined | { drag: Drag; pointerId: number };\n\n\tconst end = (): void => {\n\t\tif (!held) return;\n\n\t\tconst { drag } = held;\n\n\t\theld = undefined;\n\t\thandlers.toggle?.(false);\n\t\thandlers.release?.(drag);\n\t};\n\tconst whileHeld = (\n\t\ttype: 'lostpointercapture' | 'pointercancel' | 'pointermove' | 'pointerup',\n\t\thandle: (drag: Drag, event: PointerEvent) => void,\n\t): void => {\n\t\ttarget.addEventListener(\n\t\t\ttype,\n\t\t\t(event) => {\n\t\t\t\tif (held?.pointerId === event.pointerId) handle(held.drag, event);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t};\n\n\ttarget.addEventListener(\n\t\t'pointerdown',\n\t\t(event) => {\n\t\t\tif (event.button !== 0 || (held && held.pointerId !== event.pointerId)) return;\n\n\t\t\tend();\n\n\t\t\tconst drag = handlers.grab(event);\n\t\t\tif (drag === undefined) return;\n\n\t\t\theld = { drag, pointerId: event.pointerId };\n\t\t\thandlers.toggle?.(true);\n\t\t\tcapturePointer(target, event.pointerId);\n\t\t},\n\t\t{ signal },\n\t);\n\twhileHeld('pointermove', handlers.move);\n\twhileHeld('pointercancel', (drag) => {\n\t\thandlers.cancel?.(drag);\n\t\tend();\n\t});\n\twhileHeld('lostpointercapture', end);\n\twhileHeld('pointerup', (drag, event) => {\n\t\tend();\n\t\thandlers.lift?.(drag, event);\n\t});\n\tsignal.addEventListener('abort', end, { once: true });\n\n\treturn { current: () => held?.drag, end };\n}\n"],"mappings":";AAcA,SAAgB,eAAe,QAAqB,WAAyB;CAC5E,IAAI;EACH,OAAO,kBAAkB,SAAS;CACnC,QAAQ,CAER;AACD;AAEA,SAAgB,SACf,QACA,UACA,QACoB;CACpB,IAAI;CAEJ,MAAM,YAAkB;EACvB,IAAI,CAAC,MAAM;EAEX,MAAM,EAAE,SAAS;EAEjB,OAAO;EACP,SAAS,SAAS,KAAK;EACvB,SAAS,UAAU,IAAI;CACxB;CACA,MAAM,aACL,MACA,WACU;EACV,OAAO,iBACN,OACC,UAAU;GACV,IAAI,MAAM,cAAc,MAAM,WAAW,OAAO,KAAK,MAAM,KAAK;EACjE,GACA,EAAE,OAAO,CACV;CACD;CAEA,OAAO,iBACN,gBACC,UAAU;EACV,IAAI,MAAM,WAAW,KAAM,QAAQ,KAAK,cAAc,MAAM,WAAY;EAExE,IAAI;EAEJ,MAAM,OAAO,SAAS,KAAK,KAAK;EAChC,IAAI,SAAS,QAAW;EAExB,OAAO;GAAE;GAAM,WAAW,MAAM;EAAU;EAC1C,SAAS,SAAS,IAAI;EACtB,eAAe,QAAQ,MAAM,SAAS;CACvC,GACA,EAAE,OAAO,CACV;CACA,UAAU,eAAe,SAAS,IAAI;CACtC,UAAU,kBAAkB,SAAS;EACpC,SAAS,SAAS,IAAI;EACtB,IAAI;CACL,CAAC;CACD,UAAU,sBAAsB,GAAG;CACnC,UAAU,cAAc,MAAM,UAAU;EACvC,IAAI;EACJ,SAAS,OAAO,MAAM,KAAK;CAC5B,CAAC;CACD,OAAO,iBAAiB,SAAS,KAAK,EAAE,MAAM,KAAK,CAAC;CAEpD,OAAO;EAAE,eAAe,MAAM;EAAM;CAAI;AACzC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/lib/read-ms-property.ts
|
|
2
|
+
function readMsProperty(styles, property, fallback) {
|
|
3
|
+
const text = styles.getPropertyValue(property).trim();
|
|
4
|
+
const parsed = Number.parseFloat(text);
|
|
5
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
6
|
+
return text.endsWith("ms") ? parsed : parsed * 1e3;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
export { readMsProperty };
|
|
11
|
+
//# sourceMappingURL=read-ms-property.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-ms-property.js","names":[],"sources":["../../../src/lib/read-ms-property.ts"],"sourcesContent":["// Registered `<time>` properties only; engines differ on whether one computes to s or ms\nexport function readMsProperty(\n\tstyles: CSSStyleDeclaration,\n\tproperty: string,\n\tfallback: number,\n): number {\n\tconst text = styles.getPropertyValue(property).trim();\n\t// eslint-disable-next-line unicorn/prefer-number-coercion -- parsing has to stop at the unit\n\tconst parsed = Number.parseFloat(text);\n\tif (!Number.isFinite(parsed)) return fallback;\n\n\treturn text.endsWith('ms') ? parsed : parsed * 1000;\n}\n"],"mappings":";AACA,SAAgB,eACf,QACA,UACA,UACS;CACT,MAAM,OAAO,OAAO,iBAAiB,QAAQ,CAAC,CAAC,KAAK;CAEpD,MAAM,SAAS,OAAO,WAAW,IAAI;CACrC,IAAI,CAAC,OAAO,SAAS,MAAM,GAAG,OAAO;CAErC,OAAO,KAAK,SAAS,IAAI,IAAI,SAAS,SAAS;AAChD"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { linearTaper, logTaper } from "./taper.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/spectrum-columns.ts
|
|
4
|
+
const gridDecibels = 12;
|
|
5
|
+
function binHertz({ binCount, sampleRate }) {
|
|
6
|
+
return sampleRate / 2 / binCount;
|
|
7
|
+
}
|
|
8
|
+
function columnEdges(axis, width) {
|
|
9
|
+
const edges = new Float64Array(width + 1);
|
|
10
|
+
const taper = logTaper(axis.frequencyMin, axis.frequencyMax);
|
|
11
|
+
if (!taper) return edges;
|
|
12
|
+
const hertz = binHertz(axis);
|
|
13
|
+
for (let edge = 0; edge <= width; edge += 1) edges[edge] = taper.valueAt(edge / width) / hertz;
|
|
14
|
+
return edges;
|
|
15
|
+
}
|
|
16
|
+
function columnLevels(columns, levels, { edges, floor }) {
|
|
17
|
+
const levelAt = (bin) => bin < levels.length ? levels[bin] ?? floor : floor;
|
|
18
|
+
for (let column = 0; column < columns.length; column += 1) {
|
|
19
|
+
const from = edges[column] ?? 0;
|
|
20
|
+
const to = edges[column + 1] ?? from;
|
|
21
|
+
const first = Math.ceil(from);
|
|
22
|
+
const last = Math.ceil(to) - 1;
|
|
23
|
+
if (first > last) {
|
|
24
|
+
const below = levelAt(first - 1);
|
|
25
|
+
columns[column] = below + (levelAt(first) - below) * ((from + to) / 2 - (first - 1));
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
let loudest = floor;
|
|
29
|
+
for (let bin = first; bin <= last && bin < levels.length; bin += 1) loudest = Math.max(loudest, levelAt(bin));
|
|
30
|
+
columns[column] = loudest;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function levelRows({ max, min }, height) {
|
|
34
|
+
const taper = linearTaper(min, max);
|
|
35
|
+
return (level) => Math.round((1 - taper.proportionOf(level)) * height);
|
|
36
|
+
}
|
|
37
|
+
function gridColumns({ frequencyMax, frequencyMin }, width) {
|
|
38
|
+
const taper = logTaper(frequencyMin, frequencyMax);
|
|
39
|
+
const columns = [];
|
|
40
|
+
if (!taper) return columns;
|
|
41
|
+
for (let exponent = Math.floor(Math.log10(frequencyMin)) + 1; 10 ** exponent < frequencyMax; exponent += 1) columns.push(Math.round(taper.proportionOf(10 ** exponent) * width));
|
|
42
|
+
return columns;
|
|
43
|
+
}
|
|
44
|
+
function gridRows({ max, min }, rowOf) {
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (let level = (Math.floor(min / gridDecibels) + 1) * gridDecibels; level < max; level += gridDecibels) rows.push(rowOf(level));
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
function zoneRows({ clipFrom, hotFrom }, rowOf) {
|
|
50
|
+
const clip = rowOf(clipFrom);
|
|
51
|
+
return {
|
|
52
|
+
clip,
|
|
53
|
+
hot: Math.max(clip, rowOf(hotFrom))
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function loudestBin(frame, axis) {
|
|
57
|
+
const hertz = binHertz(axis);
|
|
58
|
+
const last = Math.min(Math.floor(axis.frequencyMax / hertz), frame.length - 1);
|
|
59
|
+
let loudest = -1;
|
|
60
|
+
let decibels = -Infinity;
|
|
61
|
+
for (let bin = Math.ceil(axis.frequencyMin / hertz); bin <= last; bin += 1) {
|
|
62
|
+
const level = frame[bin] ?? NaN;
|
|
63
|
+
if (!(Number.isFinite(level) && level > decibels)) continue;
|
|
64
|
+
loudest = bin;
|
|
65
|
+
decibels = level;
|
|
66
|
+
}
|
|
67
|
+
return loudest === -1 ? void 0 : {
|
|
68
|
+
decibels,
|
|
69
|
+
frequency: loudest * hertz
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { columnEdges, columnLevels, gridColumns, gridRows, levelRows, loudestBin, zoneRows };
|
|
75
|
+
//# sourceMappingURL=spectrum-columns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spectrum-columns.js","names":[],"sources":["../../../src/lib/spectrum-columns.ts"],"sourcesContent":["import { linearTaper, logTaper } from '#lib/taper.ts';\n\nexport interface FrequencyAxis {\n\tbinCount: number;\n\tfrequencyMax: number;\n\tfrequencyMin: number;\n\tsampleRate: number;\n}\n\ninterface LevelRange {\n\tmax: number;\n\tmin: number;\n}\n\nconst gridDecibels = 12;\n\nfunction binHertz({ binCount, sampleRate }: FrequencyAxis): number {\n\treturn sampleRate / 2 / binCount;\n}\n\nexport function columnEdges(axis: FrequencyAxis, width: number): Float64Array {\n\tconst edges = new Float64Array(width + 1);\n\tconst taper = logTaper(axis.frequencyMin, axis.frequencyMax);\n\tif (!taper) return edges;\n\n\tconst hertz = binHertz(axis);\n\n\tfor (let edge = 0; edge <= width; edge += 1) edges[edge] = taper.valueAt(edge / width) / hertz;\n\n\treturn edges;\n}\n\nexport function columnLevels(\n\tcolumns: Float64Array,\n\tlevels: ArrayLike<number>,\n\t{ edges, floor }: { edges: Float64Array; floor: number },\n): void {\n\tconst levelAt = (bin: number): number => (bin < levels.length ? (levels[bin] ?? floor) : floor);\n\n\tfor (let column = 0; column < columns.length; column += 1) {\n\t\tconst from = edges[column] ?? 0;\n\t\tconst to = edges[column + 1] ?? from;\n\t\tconst first = Math.ceil(from);\n\t\tconst last = Math.ceil(to) - 1;\n\n\t\tif (first > last) {\n\t\t\tconst below = levelAt(first - 1);\n\n\t\t\tcolumns[column] = below + (levelAt(first) - below) * ((from + to) / 2 - (first - 1));\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet loudest = floor;\n\n\t\tfor (let bin = first; bin <= last && bin < levels.length; bin += 1) {\n\t\t\tloudest = Math.max(loudest, levelAt(bin));\n\t\t}\n\t\tcolumns[column] = loudest;\n\t}\n}\n\nexport function levelRows({ max, min }: LevelRange, height: number): (level: number) => number {\n\tconst taper = linearTaper(min, max);\n\n\treturn (level) => Math.round((1 - taper.proportionOf(level)) * height);\n}\n\nexport function gridColumns(\n\t{ frequencyMax, frequencyMin }: Pick<FrequencyAxis, 'frequencyMax' | 'frequencyMin'>,\n\twidth: number,\n): Array<number> {\n\tconst taper = logTaper(frequencyMin, frequencyMax);\n\tconst columns: Array<number> = [];\n\tif (!taper) return columns;\n\n\tfor (\n\t\tlet exponent = Math.floor(Math.log10(frequencyMin)) + 1;\n\t\t10 ** exponent < frequencyMax;\n\t\texponent += 1\n\t) {\n\t\tcolumns.push(Math.round(taper.proportionOf(10 ** exponent) * width));\n\t}\n\n\treturn columns;\n}\n\nexport function gridRows(\n\t{ max, min }: LevelRange,\n\trowOf: (level: number) => number,\n): Array<number> {\n\tconst rows: Array<number> = [];\n\n\tfor (\n\t\tlet level = (Math.floor(min / gridDecibels) + 1) * gridDecibels;\n\t\tlevel < max;\n\t\tlevel += gridDecibels\n\t) {\n\t\trows.push(rowOf(level));\n\t}\n\n\treturn rows;\n}\n\nexport function zoneRows(\n\t{ clipFrom, hotFrom }: { clipFrom: number; hotFrom: number },\n\trowOf: (level: number) => number,\n): { clip: number; hot: number } {\n\tconst clip = rowOf(clipFrom);\n\n\treturn { clip, hot: Math.max(clip, rowOf(hotFrom)) };\n}\n\nexport function loudestBin(\n\tframe: ArrayLike<number>,\n\taxis: FrequencyAxis,\n): undefined | { decibels: number; frequency: number } {\n\tconst hertz = binHertz(axis);\n\tconst last = Math.min(Math.floor(axis.frequencyMax / hertz), frame.length - 1);\n\tlet loudest = -1;\n\tlet decibels = -Infinity;\n\n\tfor (let bin = Math.ceil(axis.frequencyMin / hertz); bin <= last; bin += 1) {\n\t\tconst level = frame[bin] ?? NaN;\n\n\t\tif (!(Number.isFinite(level) && level > decibels)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tloudest = bin;\n\t\tdecibels = level;\n\t}\n\n\treturn loudest === -1 ? undefined : { decibels, frequency: loudest * hertz };\n}\n"],"mappings":";;;AAcA,MAAM,eAAe;AAErB,SAAS,SAAS,EAAE,UAAU,cAAqC;CAClE,OAAO,aAAa,IAAI;AACzB;AAEA,SAAgB,YAAY,MAAqB,OAA6B;CAC7E,MAAM,QAAQ,IAAI,aAAa,QAAQ,CAAC;CACxC,MAAM,QAAQ,SAAS,KAAK,cAAc,KAAK,YAAY;CAC3D,IAAI,CAAC,OAAO,OAAO;CAEnB,MAAM,QAAQ,SAAS,IAAI;CAE3B,KAAK,IAAI,OAAO,GAAG,QAAQ,OAAO,QAAQ,GAAG,MAAM,QAAQ,MAAM,QAAQ,OAAO,KAAK,IAAI;CAEzF,OAAO;AACR;AAEA,SAAgB,aACf,SACA,QACA,EAAE,OAAO,SACF;CACP,MAAM,WAAW,QAAyB,MAAM,OAAO,SAAU,OAAO,QAAQ,QAAS;CAEzF,KAAK,IAAI,SAAS,GAAG,SAAS,QAAQ,QAAQ,UAAU,GAAG;EAC1D,MAAM,OAAO,MAAM,WAAW;EAC9B,MAAM,KAAK,MAAM,SAAS,MAAM;EAChC,MAAM,QAAQ,KAAK,KAAK,IAAI;EAC5B,MAAM,OAAO,KAAK,KAAK,EAAE,IAAI;EAE7B,IAAI,QAAQ,MAAM;GACjB,MAAM,QAAQ,QAAQ,QAAQ,CAAC;GAE/B,QAAQ,UAAU,SAAS,QAAQ,KAAK,IAAI,WAAW,OAAO,MAAM,KAAK,QAAQ;GACjF;EACD;EAEA,IAAI,UAAU;EAEd,KAAK,IAAI,MAAM,OAAO,OAAO,QAAQ,MAAM,OAAO,QAAQ,OAAO,GAChE,UAAU,KAAK,IAAI,SAAS,QAAQ,GAAG,CAAC;EAEzC,QAAQ,UAAU;CACnB;AACD;AAEA,SAAgB,UAAU,EAAE,KAAK,OAAmB,QAA2C;CAC9F,MAAM,QAAQ,YAAY,KAAK,GAAG;CAElC,QAAQ,UAAU,KAAK,OAAO,IAAI,MAAM,aAAa,KAAK,KAAK,MAAM;AACtE;AAEA,SAAgB,YACf,EAAE,cAAc,gBAChB,OACgB;CAChB,MAAM,QAAQ,SAAS,cAAc,YAAY;CACjD,MAAM,UAAyB,CAAC;CAChC,IAAI,CAAC,OAAO,OAAO;CAEnB,KACC,IAAI,WAAW,KAAK,MAAM,KAAK,MAAM,YAAY,CAAC,IAAI,GACtD,MAAM,WAAW,cACjB,YAAY,GAEZ,QAAQ,KAAK,KAAK,MAAM,MAAM,aAAa,MAAM,QAAQ,IAAI,KAAK,CAAC;CAGpE,OAAO;AACR;AAEA,SAAgB,SACf,EAAE,KAAK,OACP,OACgB;CAChB,MAAM,OAAsB,CAAC;CAE7B,KACC,IAAI,SAAS,KAAK,MAAM,MAAM,YAAY,IAAI,KAAK,cACnD,QAAQ,KACR,SAAS,cAET,KAAK,KAAK,MAAM,KAAK,CAAC;CAGvB,OAAO;AACR;AAEA,SAAgB,SACf,EAAE,UAAU,WACZ,OACgC;CAChC,MAAM,OAAO,MAAM,QAAQ;CAE3B,OAAO;EAAE;EAAM,KAAK,KAAK,IAAI,MAAM,MAAM,OAAO,CAAC;CAAE;AACpD;AAEA,SAAgB,WACf,OACA,MACsD;CACtD,MAAM,QAAQ,SAAS,IAAI;CAC3B,MAAM,OAAO,KAAK,IAAI,KAAK,MAAM,KAAK,eAAe,KAAK,GAAG,MAAM,SAAS,CAAC;CAC7E,IAAI,UAAU;CACd,IAAI,WAAW;CAEf,KAAK,IAAI,MAAM,KAAK,KAAK,KAAK,eAAe,KAAK,GAAG,OAAO,MAAM,OAAO,GAAG;EAC3E,MAAM,QAAQ,MAAM,QAAQ;EAE5B,IAAI,EAAE,OAAO,SAAS,KAAK,KAAK,QAAQ,WACvC;EAGD,UAAU;EACV,WAAW;CACZ;CAEA,OAAO,YAAY,KAAK,SAAY;EAAE;EAAU,WAAW,UAAU;CAAM;AAC5E"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ballisticsRates, fall } from "./ballistics.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/spectrum-levels.ts
|
|
4
|
+
const holdMs = 1500;
|
|
5
|
+
function createLevels(binCount) {
|
|
6
|
+
return {
|
|
7
|
+
bars: new Float64Array(binCount).fill(-Infinity),
|
|
8
|
+
heldAt: new Float64Array(binCount).fill(-Infinity),
|
|
9
|
+
holds: new Float64Array(binCount).fill(-Infinity),
|
|
10
|
+
incoming: new Float64Array(binCount).fill(-Infinity)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function receiveLevels({ incoming }, frame, floor) {
|
|
14
|
+
for (let bin = 0; bin < incoming.length; bin += 1) {
|
|
15
|
+
const level = frame[bin] ?? NaN;
|
|
16
|
+
incoming[bin] = Math.max(incoming[bin] ?? floor, Number.isFinite(level) ? level : floor);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function fallenHold(hold, bar, { elapsedMs, isHoldStepped, pastHoldMs }) {
|
|
20
|
+
if (pastHoldMs < 0) return hold;
|
|
21
|
+
if (isHoldStepped) return bar;
|
|
22
|
+
return fall(hold, {
|
|
23
|
+
fallDecibelsPerSecond: ballisticsRates.peak.fallDecibelsPerSecond,
|
|
24
|
+
floor: bar
|
|
25
|
+
}, Math.min(elapsedMs, pastHoldMs));
|
|
26
|
+
}
|
|
27
|
+
function stepLevels(levels, step) {
|
|
28
|
+
const { bars, heldAt, holds, incoming } = levels;
|
|
29
|
+
const rate = {
|
|
30
|
+
fallDecibelsPerSecond: ballisticsRates.peak.fallDecibelsPerSecond,
|
|
31
|
+
floor: step.floor
|
|
32
|
+
};
|
|
33
|
+
for (let bin = 0; bin < bars.length; bin += 1) {
|
|
34
|
+
const bar = Math.max(fall(bars[bin] ?? step.floor, rate, step.elapsedMs), incoming[bin] ?? step.floor);
|
|
35
|
+
const hold = holds[bin] ?? bar;
|
|
36
|
+
bars[bin] = bar;
|
|
37
|
+
incoming[bin] = -Infinity;
|
|
38
|
+
if (!step.isHeld || bar >= hold) {
|
|
39
|
+
holds[bin] = bar;
|
|
40
|
+
heldAt[bin] = step.nowMs;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const pastHoldMs = step.nowMs - (heldAt[bin] ?? step.nowMs) - holdMs;
|
|
44
|
+
holds[bin] = fallenHold(hold, bar, {
|
|
45
|
+
...step,
|
|
46
|
+
pastHoldMs
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function areSettled({ holds }, floor) {
|
|
51
|
+
return holds.every((hold) => hold <= floor);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { areSettled, createLevels, receiveLevels, stepLevels };
|
|
56
|
+
//# sourceMappingURL=spectrum-levels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spectrum-levels.js","names":[],"sources":["../../../src/lib/spectrum-levels.ts"],"sourcesContent":["import { ballisticsRates, fall } from '#lib/ballistics.ts';\n\nexport interface SpectrumLevels {\n\tbars: Float64Array;\n\theldAt: Float64Array;\n\tholds: Float64Array;\n\tincoming: Float64Array;\n}\n\ninterface LevelStep {\n\telapsedMs: number;\n\tfloor: number;\n\tisHeld: boolean;\n\tisHoldStepped: boolean;\n\tnowMs: number;\n}\n\nconst holdMs = 1500;\n\nexport function createLevels(binCount: number): SpectrumLevels {\n\treturn {\n\t\tbars: new Float64Array(binCount).fill(-Infinity),\n\t\theldAt: new Float64Array(binCount).fill(-Infinity),\n\t\tholds: new Float64Array(binCount).fill(-Infinity),\n\t\tincoming: new Float64Array(binCount).fill(-Infinity),\n\t};\n}\n\n// Collected apart from the bars, so a level received this frame is drawn before it falls\nexport function receiveLevels(\n\t{ incoming }: SpectrumLevels,\n\tframe: ArrayLike<number>,\n\tfloor: number,\n): void {\n\tfor (let bin = 0; bin < incoming.length; bin += 1) {\n\t\tconst level = frame[bin] ?? NaN;\n\n\t\tincoming[bin] = Math.max(incoming[bin] ?? floor, Number.isFinite(level) ? level : floor);\n\t}\n}\n\nfunction fallenHold(\n\thold: number,\n\tbar: number,\n\t{\n\t\telapsedMs,\n\t\tisHoldStepped,\n\t\tpastHoldMs,\n\t}: Pick<LevelStep, 'elapsedMs' | 'isHoldStepped'> & {\n\t\tpastHoldMs: number;\n\t},\n): number {\n\tif (pastHoldMs < 0) return hold;\n\tif (isHoldStepped) return bar;\n\n\treturn fall(\n\t\thold,\n\t\t{ fallDecibelsPerSecond: ballisticsRates.peak.fallDecibelsPerSecond, floor: bar },\n\t\tMath.min(elapsedMs, pastHoldMs),\n\t);\n}\n\nexport function stepLevels(levels: SpectrumLevels, step: LevelStep): void {\n\tconst { bars, heldAt, holds, incoming } = levels;\n\tconst rate = {\n\t\tfallDecibelsPerSecond: ballisticsRates.peak.fallDecibelsPerSecond,\n\t\tfloor: step.floor,\n\t};\n\n\tfor (let bin = 0; bin < bars.length; bin += 1) {\n\t\tconst bar = Math.max(\n\t\t\tfall(bars[bin] ?? step.floor, rate, step.elapsedMs),\n\t\t\tincoming[bin] ?? step.floor,\n\t\t);\n\t\tconst hold = holds[bin] ?? bar;\n\n\t\tbars[bin] = bar;\n\t\tincoming[bin] = -Infinity;\n\t\tif (!step.isHeld || bar >= hold) {\n\t\t\tholds[bin] = bar;\n\t\t\theldAt[bin] = step.nowMs;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst pastHoldMs = step.nowMs - (heldAt[bin] ?? step.nowMs) - holdMs;\n\n\t\tholds[bin] = fallenHold(hold, bar, { ...step, pastHoldMs });\n\t}\n}\n\n// A hold never sits under its bar, so the holds alone say whether anything is left to fall\nexport function areSettled({ holds }: SpectrumLevels, floor: number): boolean {\n\treturn holds.every((hold) => hold <= floor);\n}\n"],"mappings":";;;AAiBA,MAAM,SAAS;AAEf,SAAgB,aAAa,UAAkC;CAC9D,OAAO;EACN,MAAM,IAAI,aAAa,QAAQ,CAAC,CAAC,KAAK,SAAS;EAC/C,QAAQ,IAAI,aAAa,QAAQ,CAAC,CAAC,KAAK,SAAS;EACjD,OAAO,IAAI,aAAa,QAAQ,CAAC,CAAC,KAAK,SAAS;EAChD,UAAU,IAAI,aAAa,QAAQ,CAAC,CAAC,KAAK,SAAS;CACpD;AACD;AAGA,SAAgB,cACf,EAAE,YACF,OACA,OACO;CACP,KAAK,IAAI,MAAM,GAAG,MAAM,SAAS,QAAQ,OAAO,GAAG;EAClD,MAAM,QAAQ,MAAM,QAAQ;EAE5B,SAAS,OAAO,KAAK,IAAI,SAAS,QAAQ,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ,KAAK;CACxF;AACD;AAEA,SAAS,WACR,MACA,KACA,EACC,WACA,eACA,cAIQ;CACT,IAAI,aAAa,GAAG,OAAO;CAC3B,IAAI,eAAe,OAAO;CAE1B,OAAO,KACN,MACA;EAAE,uBAAuB,gBAAgB,KAAK;EAAuB,OAAO;CAAI,GAChF,KAAK,IAAI,WAAW,UAAU,CAC/B;AACD;AAEA,SAAgB,WAAW,QAAwB,MAAuB;CACzE,MAAM,EAAE,MAAM,QAAQ,OAAO,aAAa;CAC1C,MAAM,OAAO;EACZ,uBAAuB,gBAAgB,KAAK;EAC5C,OAAO,KAAK;CACb;CAEA,KAAK,IAAI,MAAM,GAAG,MAAM,KAAK,QAAQ,OAAO,GAAG;EAC9C,MAAM,MAAM,KAAK,IAChB,KAAK,KAAK,QAAQ,KAAK,OAAO,MAAM,KAAK,SAAS,GAClD,SAAS,QAAQ,KAAK,KACvB;EACA,MAAM,OAAO,MAAM,QAAQ;EAE3B,KAAK,OAAO;EACZ,SAAS,OAAO;EAChB,IAAI,CAAC,KAAK,UAAU,OAAO,MAAM;GAChC,MAAM,OAAO;GACb,OAAO,OAAO,KAAK;GACnB;EACD;EAEA,MAAM,aAAa,KAAK,SAAS,OAAO,QAAQ,KAAK,SAAS;EAE9D,MAAM,OAAO,WAAW,MAAM,KAAK;GAAE,GAAG;GAAM;EAAW,CAAC;CAC3D;AACD;AAGA,SAAgB,WAAW,EAAE,SAAyB,OAAwB;CAC7E,OAAO,MAAM,OAAO,SAAS,QAAQ,KAAK;AAC3C"}
|
|
@@ -18,6 +18,17 @@ function stripBars(peaks, size, grid) {
|
|
|
18
18
|
};
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
+
function stripGroove(size, ratio) {
|
|
22
|
+
if (ratio <= 0) return void 0;
|
|
23
|
+
const height = Math.max(1, Math.round(size.height * ratio));
|
|
24
|
+
return {
|
|
25
|
+
height,
|
|
26
|
+
radius: height / 2,
|
|
27
|
+
width: size.width,
|
|
28
|
+
x: 0,
|
|
29
|
+
y: Math.round((size.height - height) / 2)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
21
32
|
function stripRegions(state, widthPx) {
|
|
22
33
|
const px = (proportion) => Math.round((Number.isFinite(proportion) ? proportion : 0) * widthPx);
|
|
23
34
|
const buffered = state.buffered.map(([start, end]) => [px(start), px(end)]).filter(([start, end]) => end > start);
|
|
@@ -45,5 +56,5 @@ function stripRegions(state, widthPx) {
|
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
//#endregion
|
|
48
|
-
export { stripBars, stripRegions };
|
|
59
|
+
export { stripBars, stripGroove, stripRegions };
|
|
49
60
|
//# sourceMappingURL=strip-scene.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strip-scene.js","names":[],"sources":["../../../src/lib/strip-scene.ts"],"sourcesContent":["import type { SurfaceSize } from '#lib/canvas-surface.ts';\n\nimport { clampProportion } from '#lib/math.ts';\nimport { resamplePeaks } from '#lib/resample-peaks.ts';\n\ninterface BarRect {\n\theight: number;\n\tradius: number;\n\twidth: number;\n\tx: number;\n\ty: number;\n}\n\ninterface BarGrid {\n\tgapRatio: number;\n\tpitch: number;\n\tradiusRatio: number;\n}\n\ninterface RegionState {\n\tbuffered: ReadonlyArray<[number, number]>;\n\tplayed: number;\n\tscrub?: number;\n}\n\nexport interface StripRegions {\n\tkey: string;\n\tregions: Array<{ from: number; kind: 'buffered' | 'played' | 'scrub'; to: number }>;\n}\n\n// Whole device pixels, or bars alias unevenly and Skia's CPU raster crawls\nexport function stripBars(\n\tpeaks: ArrayLike<number>,\n\tsize: SurfaceSize,\n\tgrid: BarGrid,\n): Array<BarRect> {\n\tconst pitch = Math.max(1, Math.round(grid.pitch * size.dpr));\n\tconst gap = Math.min(pitch - 1, Math.max(0, Math.round(pitch * grid.gapRatio)));\n\tconst width = pitch - gap;\n\tconst count = Math.max(0, Math.floor((size.width + gap) / pitch));\n\n\treturn resamplePeaks(peaks, count).map((peak, index) => {\n\t\tconst height = Math.max(1, Math.round(clampProportion(peak) * size.height));\n\n\t\treturn {\n\t\t\theight,\n\t\t\tradius: width * grid.radiusRatio,\n\t\t\twidth,\n\t\t\tx: index * pitch,\n\t\t\ty: Math.round((size.height - height) / 2),\n\t\t};\n\t});\n}\n\nexport function stripRegions(state: RegionState, widthPx: number): StripRegions {\n\tconst px = (proportion: number): number =>\n\t\tMath.round((Number.isFinite(proportion) ? proportion : 0) * widthPx);\n\tconst buffered = state.buffered\n\t\t.map(([start, end]): [number, number] => [px(start), px(end)])\n\t\t.filter(([start, end]) => end > start);\n\tconst played = px(state.played);\n\tconst scrub = state.scrub === undefined ? played : px(state.scrub);\n\tconst regions: StripRegions['regions'] = buffered.map(([from, to]) => ({\n\t\tfrom,\n\t\tkind: 'buffered',\n\t\tto,\n\t}));\n\n\tif (played > 0) regions.push({ from: 0, kind: 'played', to: played });\n\tif (scrub !== played) {\n\t\tregions.push({ from: Math.min(played, scrub), kind: 'scrub', to: Math.max(played, scrub) });\n\t}\n\n\treturn { key: `${String(played)}:${String(scrub)}:${buffered.join(',')}`, regions };\n}\n"],"mappings":";;;;AA+BA,SAAgB,UACf,OACA,MACA,MACiB;CACjB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,QAAQ,KAAK,GAAG,CAAC;CAC3D,MAAM,MAAM,KAAK,IAAI,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,KAAK,QAAQ,CAAC,CAAC;CAC9E,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,QAAQ,OAAO,KAAK,CAAC;CAEhE,OAAO,cAAc,OAAO,KAAK,CAAC,CAAC,KAAK,MAAM,UAAU;EACvD,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,gBAAgB,IAAI,IAAI,KAAK,MAAM,CAAC;EAE1E,OAAO;GACN;GACA,QAAQ,QAAQ,KAAK;GACrB;GACA,GAAG,QAAQ;GACX,GAAG,KAAK,OAAO,KAAK,SAAS,UAAU,CAAC;EACzC;CACD,CAAC;AACF;AAEA,SAAgB,aAAa,OAAoB,SAA+B;CAC/E,MAAM,MAAM,eACX,KAAK,OAAO,OAAO,SAAS,UAAU,IAAI,aAAa,KAAK,OAAO;CACpE,MAAM,WAAW,MAAM,SACrB,KAAK,CAAC,OAAO,SAA2B,CAAC,GAAG,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAC7D,QAAQ,CAAC,OAAO,SAAS,MAAM,KAAK;CACtC,MAAM,SAAS,GAAG,MAAM,MAAM;CAC9B,MAAM,QAAQ,MAAM,UAAU,SAAY,SAAS,GAAG,MAAM,KAAK;CACjE,MAAM,UAAmC,SAAS,KAAK,CAAC,MAAM,SAAS;EACtE;EACA,MAAM;EACN;CACD,EAAE;CAEF,IAAI,SAAS,GAAG,QAAQ,KAAK;EAAE,MAAM;EAAG,MAAM;EAAU,IAAI;CAAO,CAAC;CACpE,IAAI,UAAU,QACb,QAAQ,KAAK;EAAE,MAAM,KAAK,IAAI,QAAQ,KAAK;EAAG,MAAM;EAAS,IAAI,KAAK,IAAI,QAAQ,KAAK;CAAE,CAAC;CAG3F,OAAO;EAAE,KAAK,GAAG,OAAO,MAAM,EAAE,GAAG,OAAO,KAAK,EAAE,GAAG,SAAS,KAAK,GAAG;EAAK;CAAQ;AACnF"}
|
|
1
|
+
{"version":3,"file":"strip-scene.js","names":[],"sources":["../../../src/lib/strip-scene.ts"],"sourcesContent":["import type { SurfaceSize } from '#lib/canvas-surface.ts';\n\nimport { clampProportion } from '#lib/math.ts';\nimport { resamplePeaks } from '#lib/resample-peaks.ts';\n\ninterface BarRect {\n\theight: number;\n\tradius: number;\n\twidth: number;\n\tx: number;\n\ty: number;\n}\n\ninterface BarGrid {\n\tgapRatio: number;\n\tpitch: number;\n\tradiusRatio: number;\n}\n\ninterface RegionState {\n\tbuffered: ReadonlyArray<[number, number]>;\n\tplayed: number;\n\tscrub?: number;\n}\n\nexport interface StripRegions {\n\tkey: string;\n\tregions: Array<{ from: number; kind: 'buffered' | 'played' | 'scrub'; to: number }>;\n}\n\n// Whole device pixels, or bars alias unevenly and Skia's CPU raster crawls\nexport function stripBars(\n\tpeaks: ArrayLike<number>,\n\tsize: SurfaceSize,\n\tgrid: BarGrid,\n): Array<BarRect> {\n\tconst pitch = Math.max(1, Math.round(grid.pitch * size.dpr));\n\tconst gap = Math.min(pitch - 1, Math.max(0, Math.round(pitch * grid.gapRatio)));\n\tconst width = pitch - gap;\n\tconst count = Math.max(0, Math.floor((size.width + gap) / pitch));\n\n\treturn resamplePeaks(peaks, count).map((peak, index) => {\n\t\tconst height = Math.max(1, Math.round(clampProportion(peak) * size.height));\n\n\t\treturn {\n\t\t\theight,\n\t\t\tradius: width * grid.radiusRatio,\n\t\t\twidth,\n\t\t\tx: index * pitch,\n\t\t\ty: Math.round((size.height - height) / 2),\n\t\t};\n\t});\n}\n\nexport function stripGroove(size: SurfaceSize, ratio: number): BarRect | undefined {\n\tif (ratio <= 0) return undefined;\n\n\tconst height = Math.max(1, Math.round(size.height * ratio));\n\n\treturn {\n\t\theight,\n\t\tradius: height / 2,\n\t\twidth: size.width,\n\t\tx: 0,\n\t\ty: Math.round((size.height - height) / 2),\n\t};\n}\n\nexport function stripRegions(state: RegionState, widthPx: number): StripRegions {\n\tconst px = (proportion: number): number =>\n\t\tMath.round((Number.isFinite(proportion) ? proportion : 0) * widthPx);\n\tconst buffered = state.buffered\n\t\t.map(([start, end]): [number, number] => [px(start), px(end)])\n\t\t.filter(([start, end]) => end > start);\n\tconst played = px(state.played);\n\tconst scrub = state.scrub === undefined ? played : px(state.scrub);\n\tconst regions: StripRegions['regions'] = buffered.map(([from, to]) => ({\n\t\tfrom,\n\t\tkind: 'buffered',\n\t\tto,\n\t}));\n\n\tif (played > 0) regions.push({ from: 0, kind: 'played', to: played });\n\tif (scrub !== played) {\n\t\tregions.push({ from: Math.min(played, scrub), kind: 'scrub', to: Math.max(played, scrub) });\n\t}\n\n\treturn { key: `${String(played)}:${String(scrub)}:${buffered.join(',')}`, regions };\n}\n"],"mappings":";;;;AA+BA,SAAgB,UACf,OACA,MACA,MACiB;CACjB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,QAAQ,KAAK,GAAG,CAAC;CAC3D,MAAM,MAAM,KAAK,IAAI,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,KAAK,QAAQ,CAAC,CAAC;CAC9E,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,QAAQ,OAAO,KAAK,CAAC;CAEhE,OAAO,cAAc,OAAO,KAAK,CAAC,CAAC,KAAK,MAAM,UAAU;EACvD,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,gBAAgB,IAAI,IAAI,KAAK,MAAM,CAAC;EAE1E,OAAO;GACN;GACA,QAAQ,QAAQ,KAAK;GACrB;GACA,GAAG,QAAQ;GACX,GAAG,KAAK,OAAO,KAAK,SAAS,UAAU,CAAC;EACzC;CACD,CAAC;AACF;AAEA,SAAgB,YAAY,MAAmB,OAAoC;CAClF,IAAI,SAAS,GAAG,OAAO;CAEvB,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,SAAS,KAAK,CAAC;CAE1D,OAAO;EACN;EACA,QAAQ,SAAS;EACjB,OAAO,KAAK;EACZ,GAAG;EACH,GAAG,KAAK,OAAO,KAAK,SAAS,UAAU,CAAC;CACzC;AACD;AAEA,SAAgB,aAAa,OAAoB,SAA+B;CAC/E,MAAM,MAAM,eACX,KAAK,OAAO,OAAO,SAAS,UAAU,IAAI,aAAa,KAAK,OAAO;CACpE,MAAM,WAAW,MAAM,SACrB,KAAK,CAAC,OAAO,SAA2B,CAAC,GAAG,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAC7D,QAAQ,CAAC,OAAO,SAAS,MAAM,KAAK;CACtC,MAAM,SAAS,GAAG,MAAM,MAAM;CAC9B,MAAM,QAAQ,MAAM,UAAU,SAAY,SAAS,GAAG,MAAM,KAAK;CACjE,MAAM,UAAmC,SAAS,KAAK,CAAC,MAAM,SAAS;EACtE;EACA,MAAM;EACN;CACD,EAAE;CAEF,IAAI,SAAS,GAAG,QAAQ,KAAK;EAAE,MAAM;EAAG,MAAM;EAAU,IAAI;CAAO,CAAC;CACpE,IAAI,UAAU,QACb,QAAQ,KAAK;EAAE,MAAM,KAAK,IAAI,QAAQ,KAAK;EAAG,MAAM;EAAS,IAAI,KAAK,IAAI,QAAQ,KAAK;CAAE,CAAC;CAG3F,OAAO;EAAE,KAAK,GAAG,OAAO,MAAM,EAAE,GAAG,OAAO,KAAK,EAAE,GAAG,SAAS,KAAK,GAAG;EAAK;CAAQ;AACnF"}
|
|
@@ -3,10 +3,14 @@ function readRegions(regions) {
|
|
|
3
3
|
if ("start" in regions) return Array.from({ length: regions.length }, (_entry, index) => [regions.start(index), regions.end(index)]);
|
|
4
4
|
return [...regions].map(([start, end]) => [start, end]);
|
|
5
5
|
}
|
|
6
|
+
function dueRegions(regions, listedMs, [nowMs, delayMs]) {
|
|
7
|
+
if (delayMs <= 0) return [...regions];
|
|
8
|
+
return regions.filter((region) => nowMs - (listedMs.get(region.join(":")) ?? nowMs) >= delayMs);
|
|
9
|
+
}
|
|
6
10
|
function sortedRegions(regions) {
|
|
7
11
|
return regions ? readRegions(regions).toSorted((first, second) => first[0] - second[0]) : [];
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
//#endregion
|
|
11
|
-
export { readRegions, sortedRegions };
|
|
15
|
+
export { dueRegions, readRegions, sortedRegions };
|
|
12
16
|
//# sourceMappingURL=time-regions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-regions.js","names":[],"sources":["../../../src/lib/time-regions.ts"],"sourcesContent":["export 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 sortedRegions(regions: TimeRegions | undefined): Array<[number, number]> {\n\treturn regions ? readRegions(regions).toSorted((first, second) => first[0] - second[0]) : [];\n}\n"],"mappings":";AAEA,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,cAAc,SAA2D;CACxF,OAAO,UAAU,YAAY,OAAO,CAAC,CAAC,UAAU,OAAO,WAAW,MAAM,KAAK,OAAO,EAAE,IAAI,CAAC;AAC5F"}
|
|
1
|
+
{"version":3,"file":"time-regions.js","names":[],"sources":["../../../src/lib/time-regions.ts"],"sourcesContent":["export 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":";AAEA,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"}
|
package/dist/styles/button.css
CHANGED
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
/* Before the latched rule too; a button with an LED blinks the LED and keeps its legend */
|
|
82
82
|
:where([busy]) > .sonic-button:not(:disabled, [aria-disabled='true'], :has(.sonic-led)) {
|
|
83
83
|
color: color-mix(in oklab, var(--_sonic-lit) 55%, var(--_sonic-ink));
|
|
84
|
+
transition-delay: var(--sonic-button-busy-delay, 0s);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
.sonic-button:active:where(:not(:disabled, [aria-disabled='true'])),
|
|
@@ -226,12 +227,17 @@
|
|
|
226
227
|
display: none;
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
.sonic-button-cap > [data-sonic-hidden] {
|
|
231
|
+
display: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
229
234
|
[busy]
|
|
230
235
|
> .sonic-button:not(:disabled, [aria-disabled='true'], :has(.sonic-led))
|
|
231
236
|
> .sonic-button-cap
|
|
232
237
|
> *,
|
|
233
238
|
[busy] > .sonic-button:not(:disabled, [aria-disabled='true']) .sonic-led::after {
|
|
234
|
-
animation: sonic-button-busy 500ms ease-in-out infinite
|
|
239
|
+
animation: sonic-button-busy 500ms ease-in-out var(--sonic-button-busy-delay, 0s) infinite
|
|
240
|
+
alternate;
|
|
235
241
|
|
|
236
242
|
@media (prefers-reduced-motion: reduce) {
|
|
237
243
|
animation: none;
|