@xsynaptic/sonic-ui 0.24.1 → 0.26.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/README.md +6 -4
  3. package/dist/default/define/region.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/button.js +3 -0
  6. package/dist/default/elements/dial.js +3 -0
  7. package/dist/default/elements/envelope.js +14 -11
  8. package/dist/default/elements/form-element.js +10 -0
  9. package/dist/default/elements/meter.js +3 -0
  10. package/dist/default/elements/number.js +3 -0
  11. package/dist/default/elements/radio-group.js +4 -1
  12. package/dist/default/elements/readout.js +1 -1
  13. package/dist/default/elements/region-layer.js +253 -0
  14. package/dist/default/elements/region.js +161 -0
  15. package/dist/default/elements/slider.js +3 -0
  16. package/dist/default/elements/sonic-element.js +7 -9
  17. package/dist/default/elements/split.js +2 -2
  18. package/dist/default/elements/toggle.js +4 -4
  19. package/dist/default/elements/value-element.js +21 -3
  20. package/dist/default/elements/value-gestures.js +7 -5
  21. package/dist/default/elements/wave-element.js +15 -3
  22. package/dist/default/elements/waveform.js +38 -2
  23. package/dist/default/elements/wavestrip.js +90 -17
  24. package/dist/default/elements/xy.js +6 -11
  25. package/dist/default/index.js +2 -1
  26. package/dist/default/lib/canvas-surface.js +22 -1
  27. package/dist/default/lib/clock.js +1 -0
  28. package/dist/default/lib/drag-step.js +1 -1
  29. package/dist/default/lib/key-press.js +2 -2
  30. package/dist/default/lib/marker-band.js +40 -29
  31. package/dist/default/lib/region-drag.js +48 -0
  32. package/dist/dev/define/region.d.ts +2 -0
  33. package/dist/dev/define/region.js +8 -0
  34. package/dist/dev/define/region.js.map +1 -0
  35. package/dist/dev/define.d.ts +3 -1
  36. package/dist/dev/define.js +1 -0
  37. package/dist/dev/elements/button.d.ts +1 -0
  38. package/dist/dev/elements/button.js +3 -0
  39. package/dist/dev/elements/button.js.map +1 -1
  40. package/dist/dev/elements/dial.d.ts +1 -0
  41. package/dist/dev/elements/dial.js +3 -0
  42. package/dist/dev/elements/dial.js.map +1 -1
  43. package/dist/dev/elements/envelope.d.ts +1 -0
  44. package/dist/dev/elements/envelope.js +14 -11
  45. package/dist/dev/elements/envelope.js.map +1 -1
  46. package/dist/dev/elements/form-element.d.ts +1 -0
  47. package/dist/dev/elements/form-element.js +10 -0
  48. package/dist/dev/elements/form-element.js.map +1 -1
  49. package/dist/dev/elements/meter.d.ts +1 -0
  50. package/dist/dev/elements/meter.js +3 -0
  51. package/dist/dev/elements/meter.js.map +1 -1
  52. package/dist/dev/elements/number.d.ts +1 -0
  53. package/dist/dev/elements/number.js +3 -0
  54. package/dist/dev/elements/number.js.map +1 -1
  55. package/dist/dev/elements/radio-group.d.ts +1 -0
  56. package/dist/dev/elements/radio-group.js +4 -1
  57. package/dist/dev/elements/radio-group.js.map +1 -1
  58. package/dist/dev/elements/readout.js +1 -1
  59. package/dist/dev/elements/readout.js.map +1 -1
  60. package/dist/dev/elements/region-layer.js +254 -0
  61. package/dist/dev/elements/region-layer.js.map +1 -0
  62. package/dist/dev/elements/region.d.ts +30 -0
  63. package/dist/dev/elements/region.js +169 -0
  64. package/dist/dev/elements/region.js.map +1 -0
  65. package/dist/dev/elements/slider.d.ts +1 -0
  66. package/dist/dev/elements/slider.js +3 -0
  67. package/dist/dev/elements/slider.js.map +1 -1
  68. package/dist/dev/elements/sonic-element.d.ts +2 -0
  69. package/dist/dev/elements/sonic-element.js +8 -10
  70. package/dist/dev/elements/sonic-element.js.map +1 -1
  71. package/dist/dev/elements/split.js +2 -2
  72. package/dist/dev/elements/split.js.map +1 -1
  73. package/dist/dev/elements/toggle.js +4 -4
  74. package/dist/dev/elements/toggle.js.map +1 -1
  75. package/dist/dev/elements/value-element.d.ts +3 -1
  76. package/dist/dev/elements/value-element.js +21 -3
  77. package/dist/dev/elements/value-element.js.map +1 -1
  78. package/dist/dev/elements/value-gestures.js +7 -5
  79. package/dist/dev/elements/value-gestures.js.map +1 -1
  80. package/dist/dev/elements/wave-element.d.ts +8 -1
  81. package/dist/dev/elements/wave-element.js +15 -3
  82. package/dist/dev/elements/wave-element.js.map +1 -1
  83. package/dist/dev/elements/waveform.d.ts +7 -0
  84. package/dist/dev/elements/waveform.js +38 -2
  85. package/dist/dev/elements/waveform.js.map +1 -1
  86. package/dist/dev/elements/wavestrip.d.ts +17 -1
  87. package/dist/dev/elements/wavestrip.js +90 -17
  88. package/dist/dev/elements/wavestrip.js.map +1 -1
  89. package/dist/dev/elements/xy.d.ts +1 -0
  90. package/dist/dev/elements/xy.js +6 -11
  91. package/dist/dev/elements/xy.js.map +1 -1
  92. package/dist/dev/index.d.ts +3 -2
  93. package/dist/dev/index.js +2 -1
  94. package/dist/dev/lib/canvas-surface.js +22 -1
  95. package/dist/dev/lib/canvas-surface.js.map +1 -1
  96. package/dist/dev/lib/clock.js +1 -0
  97. package/dist/dev/lib/clock.js.map +1 -1
  98. package/dist/dev/lib/drag-step.js +1 -1
  99. package/dist/dev/lib/drag-step.js.map +1 -1
  100. package/dist/dev/lib/key-press.js +2 -2
  101. package/dist/dev/lib/key-press.js.map +1 -1
  102. package/dist/dev/lib/marker-band.d.ts +7 -0
  103. package/dist/dev/lib/marker-band.js +40 -29
  104. package/dist/dev/lib/marker-band.js.map +1 -1
  105. package/dist/dev/lib/region-drag.js +49 -0
  106. package/dist/dev/lib/region-drag.js.map +1 -0
  107. package/dist/styles/button.css +23 -13
  108. package/dist/styles/controls.css +1 -0
  109. package/dist/styles/dial.css +41 -24
  110. package/dist/styles/envelope.css +12 -5
  111. package/dist/styles/led.css +8 -8
  112. package/dist/styles/material/arc.css +2 -2
  113. package/dist/styles/material/cap.css +34 -13
  114. package/dist/styles/material/glass.css +1 -1
  115. package/dist/styles/material/groove.css +3 -3
  116. package/dist/styles/material/keycap.css +2 -2
  117. package/dist/styles/material/lens.css +1 -1
  118. package/dist/styles/material/pane.css +20 -6
  119. package/dist/styles/material/readout.css +14 -5
  120. package/dist/styles/material/{bracket.css → reticle.css} +15 -8
  121. package/dist/styles/material/well.css +2 -2
  122. package/dist/styles/material.css +1 -1
  123. package/dist/styles/meter.css +29 -16
  124. package/dist/styles/number.css +3 -2
  125. package/dist/styles/panel.css +3 -3
  126. package/dist/styles/region.css +94 -0
  127. package/dist/styles/ring.css +2 -1
  128. package/dist/styles/screen.css +7 -5
  129. package/dist/styles/segmented.css +5 -4
  130. package/dist/styles/slider.css +24 -15
  131. package/dist/styles/spectrum.css +9 -1
  132. package/dist/styles/switch.css +9 -2
  133. package/dist/styles/toggle.css +5 -4
  134. package/dist/styles/waveform.css +10 -3
  135. package/dist/styles/wavestrip.css +17 -8
  136. package/dist/styles/xy.css +24 -11
  137. package/package.json +3 -2
@@ -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\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\t// A colour mixed from `currentcolor` computes to the same text whatever `color` is\n\t\tconst watched = ['color', ...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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"}
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\nfunction canTakeColour(context: CanvasRenderingContext2D, colour: string): boolean {\n\tconst kept = context.fillStyle;\n\tconst isKept = (sentinel: string): boolean => {\n\t\tcontext.fillStyle = sentinel;\n\t\tcontext.fillStyle = colour;\n\n\t\treturn context.fillStyle === sentinel;\n\t};\n\tconst isRefused = isKept('#000000') && isKept('#ffffff');\n\n\tcontext.fillStyle = kept;\n\n\treturn !isRefused;\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\t// A colour mixed from `currentcolor` computes to the same text whatever `color` is\n\t\tconst watched = ['color', ...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\tthis.#resolve(\n\t\t\t\t\tstyles\n\t\t\t\t\t\t.getPropertyValue(property)\n\t\t\t\t\t\t.trim()\n\t\t\t\t\t\t.replaceAll('currentcolor', () => styles.color),\n\t\t\t\t),\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#resolve(colour: string): string {\n\t\tconst context = this.#context;\n\t\tif (!context || canTakeColour(context, colour)) return colour;\n\n\t\tconst probe = document.createElement('span');\n\n\t\tprobe.style.color = colour;\n\t\tthis.#options.canvas.append(probe);\n\n\t\tconst resolved = getComputedStyle(probe).color;\n\n\t\tprobe.remove();\n\n\t\treturn resolved;\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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"}
@@ -1,6 +1,7 @@
1
1
  //#region src/lib/clock.ts
2
2
  const sign = /^[-−]/;
3
3
  function formatClock(seconds) {
4
+ if (!Number.isFinite(seconds)) return "";
4
5
  const total = Math.floor(Math.abs(seconds));
5
6
  const hours = Math.floor(total / 3600);
6
7
  const minutes = Math.floor(total / 60) % 60;
@@ -1 +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"}
1
+ {"version":3,"file":"clock.js","names":[],"sources":["../../../src/lib/clock.ts"],"sourcesContent":["const sign = /^[-−]/;\n\n// A time not yet known writes as nothing, the text `parseClock` reads back as NaN\nexport function formatClock(seconds: number): string {\n\tif (!Number.isFinite(seconds)) return '';\n\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;AAGb,SAAgB,YAAY,SAAyB;CACpD,IAAI,CAAC,OAAO,SAAS,OAAO,GAAG,OAAO;CAEtC,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"}
@@ -96,5 +96,5 @@ function stepDrag(mapping, state, move) {
96
96
  }
97
97
 
98
98
  //#endregion
99
- export { dragThresholdPx, startDrag, stepDrag };
99
+ export { dragThresholdPx, fineFactor, startDrag, stepDrag };
100
100
  //# sourceMappingURL=drag-step.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"drag-step.js","names":[],"sources":["../../../src/lib/drag-step.ts"],"sourcesContent":["import type { ValueMapping } from '#lib/value-mapping.ts';\n\nimport { clampProportion, wrapProportion } from '#lib/math.ts';\n\ninterface DetentHold {\n\tproportion: number;\n\tslack: number | undefined;\n\tvalue: number;\n\tzone: number;\n}\n\nexport interface DragState {\n\tdetent?: DetentHold;\n\tisEngaged: boolean;\n\tlastPosition: number;\n\trawProportion: number;\n\tstartPosition: number;\n\tthresholdPx: number;\n\ttravelPx: number;\n}\n\nexport interface DragMove {\n\tisFine: boolean;\n\tisOutside: boolean;\n\tposition: number;\n}\n\ninterface DragStart {\n\tdetent?: { value: number; zone: number };\n\tfrom: number;\n\tposition: number;\n\tproportion: number;\n\tthresholdPx: number;\n\ttravelPx: number;\n}\n\ninterface DragStep {\n\tstate: DragState;\n\tvalue: number | undefined;\n}\n\nconst fineFactor = 0.1;\nconst mouseDragThresholdPx = 3;\nconst touchDragThresholdPx = 7;\n\nexport function dragThresholdPx(pointerType: string): number {\n\treturn pointerType === 'touch' ? touchDragThresholdPx : mouseDragThresholdPx;\n}\n\nfunction isAcross(from: number, to: number, proportion: number): boolean {\n\treturn from !== proportion && (from - proportion) * (to - proportion) <= 0;\n}\n\nfunction passDetent(\n\thold: DetentHold,\n\t[from, to]: [number, number],\n\tpace: number,\n): { proportion: number | undefined; slack: number | undefined } {\n\tif (hold.slack === undefined && !isAcross(from, to, hold.proportion)) {\n\t\treturn { proportion: to, slack: undefined };\n\t}\n\n\tconst slack = (hold.slack ?? 0) + (to - hold.proportion) / pace;\n\n\tif (Math.abs(slack) <= hold.zone) return { proportion: undefined, slack };\n\n\treturn {\n\t\tproportion: hold.proportion + (slack - Math.sign(slack) * hold.zone) * pace,\n\t\tslack: undefined,\n\t};\n}\n\nfunction nearestTurn(hold: DetentHold, from: number): DetentHold {\n\treturn { ...hold, proportion: hold.proportion + Math.round(from - hold.proportion) };\n}\n\nfunction land(mapping: ValueMapping, state: DragState, proportion: number): DragStep {\n\tconst rawProportion = mapping.isWrapping\n\t\t? wrapProportion(proportion)\n\t\t: clampProportion(proportion);\n\n\treturn {\n\t\tstate: { ...state, rawProportion },\n\t\tvalue: mapping.snap(mapping.valueAt(rawProportion)),\n\t};\n}\n\nexport function startDrag(mapping: ValueMapping, start: DragStart): DragState {\n\tconst { detent: wanted, from, position, proportion, ...fixed } = start;\n\tconst state = {\n\t\t...fixed,\n\t\tisEngaged: false,\n\t\tlastPosition: position,\n\t\trawProportion: proportion,\n\t\tstartPosition: position,\n\t};\n\tif (!wanted) return state;\n\n\tconst value = mapping.snap(wanted.value);\n\tconst detent = {\n\t\tproportion: mapping.proportionOf(value),\n\t\tslack: value === from ? 0 : undefined,\n\t\tvalue,\n\t\tzone: wanted.zone,\n\t};\n\n\treturn { ...state, detent };\n}\n\nexport function stepDrag(mapping: ValueMapping, state: DragState, move: DragMove): DragStep {\n\tconst isEngaged =\n\t\tstate.isEngaged ||\n\t\tmove.isOutside ||\n\t\tMath.abs(move.position - state.startPosition) >= state.thresholdPx;\n\n\tif (!isEngaged) return { state, value: undefined };\n\n\tconst pace = move.isFine ? fineFactor : 1;\n\tconst to = state.rawProportion + ((move.position - state.lastPosition) / state.travelPx) * pace;\n\tconst moved = { ...state, isEngaged: true, lastPosition: move.position };\n\tconst { detent } = state;\n\tif (!detent) return land(mapping, moved, to);\n\n\tconst near = mapping.isWrapping ? nearestTurn(detent, state.rawProportion) : detent;\n\tconst passed = passDetent(near, [state.rawProportion, to], pace);\n\tconst held = { ...moved, detent: { ...detent, slack: passed.slack } };\n\tif (passed.proportion === undefined) {\n\t\treturn { state: { ...held, rawProportion: detent.proportion }, value: detent.value };\n\t}\n\n\treturn land(mapping, held, passed.proportion);\n}\n"],"mappings":";;;AAyCA,MAAM,aAAa;AACnB,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAE7B,SAAgB,gBAAgB,aAA6B;CAC5D,OAAO,gBAAgB,UAAU,uBAAuB;AACzD;AAEA,SAAS,SAAS,MAAc,IAAY,YAA6B;CACxE,OAAO,SAAS,eAAe,OAAO,eAAe,KAAK,eAAe;AAC1E;AAEA,SAAS,WACR,MACA,CAAC,MAAM,KACP,MACgE;CAChE,IAAI,KAAK,UAAU,UAAa,CAAC,SAAS,MAAM,IAAI,KAAK,UAAU,GAClE,OAAO;EAAE,YAAY;EAAI,OAAO;CAAU;CAG3C,MAAM,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK,cAAc;CAE3D,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,OAAO;EAAE,YAAY;EAAW;CAAM;CAExE,OAAO;EACN,YAAY,KAAK,cAAc,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,QAAQ;EACvE,OAAO;CACR;AACD;AAEA,SAAS,YAAY,MAAkB,MAA0B;CAChE,OAAO;EAAE,GAAG;EAAM,YAAY,KAAK,aAAa,KAAK,MAAM,OAAO,KAAK,UAAU;CAAE;AACpF;AAEA,SAAS,KAAK,SAAuB,OAAkB,YAA8B;CACpF,MAAM,gBAAgB,QAAQ,aAC3B,eAAe,UAAU,IACzB,gBAAgB,UAAU;CAE7B,OAAO;EACN,OAAO;GAAE,GAAG;GAAO;EAAc;EACjC,OAAO,QAAQ,KAAK,QAAQ,QAAQ,aAAa,CAAC;CACnD;AACD;AAEA,SAAgB,UAAU,SAAuB,OAA6B;CAC7E,MAAM,EAAE,QAAQ,QAAQ,MAAM,UAAU,YAAY,GAAG,UAAU;CACjE,MAAM,QAAQ;EACb,GAAG;EACH,WAAW;EACX,cAAc;EACd,eAAe;EACf,eAAe;CAChB;CACA,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK;CACvC,MAAM,SAAS;EACd,YAAY,QAAQ,aAAa,KAAK;EACtC,OAAO,UAAU,OAAO,IAAI;EAC5B;EACA,MAAM,OAAO;CACd;CAEA,OAAO;EAAE,GAAG;EAAO;CAAO;AAC3B;AAEA,SAAgB,SAAS,SAAuB,OAAkB,MAA0B;CAM3F,IAAI,EAJH,MAAM,aACN,KAAK,aACL,KAAK,IAAI,KAAK,WAAW,MAAM,aAAa,KAAK,MAAM,cAExC,OAAO;EAAE;EAAO,OAAO;CAAU;CAEjD,MAAM,OAAO,KAAK,SAAS,aAAa;CACxC,MAAM,KAAK,MAAM,iBAAkB,KAAK,WAAW,MAAM,gBAAgB,MAAM,WAAY;CAC3F,MAAM,QAAQ;EAAE,GAAG;EAAO,WAAW;EAAM,cAAc,KAAK;CAAS;CACvE,MAAM,EAAE,WAAW;CACnB,IAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,OAAO,EAAE;CAG3C,MAAM,SAAS,WADF,QAAQ,aAAa,YAAY,QAAQ,MAAM,aAAa,IAAI,QAC7C,CAAC,MAAM,eAAe,EAAE,GAAG,IAAI;CAC/D,MAAM,OAAO;EAAE,GAAG;EAAO,QAAQ;GAAE,GAAG;GAAQ,OAAO,OAAO;EAAM;CAAE;CACpE,IAAI,OAAO,eAAe,QACzB,OAAO;EAAE,OAAO;GAAE,GAAG;GAAM,eAAe,OAAO;EAAW;EAAG,OAAO,OAAO;CAAM;CAGpF,OAAO,KAAK,SAAS,MAAM,OAAO,UAAU;AAC7C"}
1
+ {"version":3,"file":"drag-step.js","names":[],"sources":["../../../src/lib/drag-step.ts"],"sourcesContent":["import type { ValueMapping } from '#lib/value-mapping.ts';\n\nimport { clampProportion, wrapProportion } from '#lib/math.ts';\n\ninterface DetentHold {\n\tproportion: number;\n\tslack: number | undefined;\n\tvalue: number;\n\tzone: number;\n}\n\nexport interface DragState {\n\tdetent?: DetentHold;\n\tisEngaged: boolean;\n\tlastPosition: number;\n\trawProportion: number;\n\tstartPosition: number;\n\tthresholdPx: number;\n\ttravelPx: number;\n}\n\nexport interface DragMove {\n\tisFine: boolean;\n\tisOutside: boolean;\n\tposition: number;\n}\n\ninterface DragStart {\n\tdetent?: { value: number; zone: number };\n\tfrom: number;\n\tposition: number;\n\tproportion: number;\n\tthresholdPx: number;\n\ttravelPx: number;\n}\n\ninterface DragStep {\n\tstate: DragState;\n\tvalue: number | undefined;\n}\n\nexport const fineFactor = 0.1;\nconst mouseDragThresholdPx = 3;\nconst touchDragThresholdPx = 7;\n\nexport function dragThresholdPx(pointerType: string): number {\n\treturn pointerType === 'touch' ? touchDragThresholdPx : mouseDragThresholdPx;\n}\n\nfunction isAcross(from: number, to: number, proportion: number): boolean {\n\treturn from !== proportion && (from - proportion) * (to - proportion) <= 0;\n}\n\nfunction passDetent(\n\thold: DetentHold,\n\t[from, to]: [number, number],\n\tpace: number,\n): { proportion: number | undefined; slack: number | undefined } {\n\tif (hold.slack === undefined && !isAcross(from, to, hold.proportion)) {\n\t\treturn { proportion: to, slack: undefined };\n\t}\n\n\tconst slack = (hold.slack ?? 0) + (to - hold.proportion) / pace;\n\n\tif (Math.abs(slack) <= hold.zone) return { proportion: undefined, slack };\n\n\treturn {\n\t\tproportion: hold.proportion + (slack - Math.sign(slack) * hold.zone) * pace,\n\t\tslack: undefined,\n\t};\n}\n\nfunction nearestTurn(hold: DetentHold, from: number): DetentHold {\n\treturn { ...hold, proportion: hold.proportion + Math.round(from - hold.proportion) };\n}\n\nfunction land(mapping: ValueMapping, state: DragState, proportion: number): DragStep {\n\tconst rawProportion = mapping.isWrapping\n\t\t? wrapProportion(proportion)\n\t\t: clampProportion(proportion);\n\n\treturn {\n\t\tstate: { ...state, rawProportion },\n\t\tvalue: mapping.snap(mapping.valueAt(rawProportion)),\n\t};\n}\n\nexport function startDrag(mapping: ValueMapping, start: DragStart): DragState {\n\tconst { detent: wanted, from, position, proportion, ...fixed } = start;\n\tconst state = {\n\t\t...fixed,\n\t\tisEngaged: false,\n\t\tlastPosition: position,\n\t\trawProportion: proportion,\n\t\tstartPosition: position,\n\t};\n\tif (!wanted) return state;\n\n\tconst value = mapping.snap(wanted.value);\n\tconst detent = {\n\t\tproportion: mapping.proportionOf(value),\n\t\tslack: value === from ? 0 : undefined,\n\t\tvalue,\n\t\tzone: wanted.zone,\n\t};\n\n\treturn { ...state, detent };\n}\n\nexport function stepDrag(mapping: ValueMapping, state: DragState, move: DragMove): DragStep {\n\tconst isEngaged =\n\t\tstate.isEngaged ||\n\t\tmove.isOutside ||\n\t\tMath.abs(move.position - state.startPosition) >= state.thresholdPx;\n\n\tif (!isEngaged) return { state, value: undefined };\n\n\tconst pace = move.isFine ? fineFactor : 1;\n\tconst to = state.rawProportion + ((move.position - state.lastPosition) / state.travelPx) * pace;\n\tconst moved = { ...state, isEngaged: true, lastPosition: move.position };\n\tconst { detent } = state;\n\tif (!detent) return land(mapping, moved, to);\n\n\tconst near = mapping.isWrapping ? nearestTurn(detent, state.rawProportion) : detent;\n\tconst passed = passDetent(near, [state.rawProportion, to], pace);\n\tconst held = { ...moved, detent: { ...detent, slack: passed.slack } };\n\tif (passed.proportion === undefined) {\n\t\treturn { state: { ...held, rawProportion: detent.proportion }, value: detent.value };\n\t}\n\n\treturn land(mapping, held, passed.proportion);\n}\n"],"mappings":";;;AAyCA,MAAa,aAAa;AAC1B,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAE7B,SAAgB,gBAAgB,aAA6B;CAC5D,OAAO,gBAAgB,UAAU,uBAAuB;AACzD;AAEA,SAAS,SAAS,MAAc,IAAY,YAA6B;CACxE,OAAO,SAAS,eAAe,OAAO,eAAe,KAAK,eAAe;AAC1E;AAEA,SAAS,WACR,MACA,CAAC,MAAM,KACP,MACgE;CAChE,IAAI,KAAK,UAAU,UAAa,CAAC,SAAS,MAAM,IAAI,KAAK,UAAU,GAClE,OAAO;EAAE,YAAY;EAAI,OAAO;CAAU;CAG3C,MAAM,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK,cAAc;CAE3D,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,OAAO;EAAE,YAAY;EAAW;CAAM;CAExE,OAAO;EACN,YAAY,KAAK,cAAc,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,QAAQ;EACvE,OAAO;CACR;AACD;AAEA,SAAS,YAAY,MAAkB,MAA0B;CAChE,OAAO;EAAE,GAAG;EAAM,YAAY,KAAK,aAAa,KAAK,MAAM,OAAO,KAAK,UAAU;CAAE;AACpF;AAEA,SAAS,KAAK,SAAuB,OAAkB,YAA8B;CACpF,MAAM,gBAAgB,QAAQ,aAC3B,eAAe,UAAU,IACzB,gBAAgB,UAAU;CAE7B,OAAO;EACN,OAAO;GAAE,GAAG;GAAO;EAAc;EACjC,OAAO,QAAQ,KAAK,QAAQ,QAAQ,aAAa,CAAC;CACnD;AACD;AAEA,SAAgB,UAAU,SAAuB,OAA6B;CAC7E,MAAM,EAAE,QAAQ,QAAQ,MAAM,UAAU,YAAY,GAAG,UAAU;CACjE,MAAM,QAAQ;EACb,GAAG;EACH,WAAW;EACX,cAAc;EACd,eAAe;EACf,eAAe;CAChB;CACA,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK;CACvC,MAAM,SAAS;EACd,YAAY,QAAQ,aAAa,KAAK;EACtC,OAAO,UAAU,OAAO,IAAI;EAC5B;EACA,MAAM,OAAO;CACd;CAEA,OAAO;EAAE,GAAG;EAAO;CAAO;AAC3B;AAEA,SAAgB,SAAS,SAAuB,OAAkB,MAA0B;CAM3F,IAAI,EAJH,MAAM,aACN,KAAK,aACL,KAAK,IAAI,KAAK,WAAW,MAAM,aAAa,KAAK,MAAM,cAExC,OAAO;EAAE;EAAO,OAAO;CAAU;CAEjD,MAAM,OAAO,KAAK,SAAS,aAAa;CACxC,MAAM,KAAK,MAAM,iBAAkB,KAAK,WAAW,MAAM,gBAAgB,MAAM,WAAY;CAC3F,MAAM,QAAQ;EAAE,GAAG;EAAO,WAAW;EAAM,cAAc,KAAK;CAAS;CACvE,MAAM,EAAE,WAAW;CACnB,IAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,OAAO,EAAE;CAG3C,MAAM,SAAS,WADF,QAAQ,aAAa,YAAY,QAAQ,MAAM,aAAa,IAAI,QAC7C,CAAC,MAAM,eAAe,EAAE,GAAG,IAAI;CAC/D,MAAM,OAAO;EAAE,GAAG;EAAO,QAAQ;GAAE,GAAG;GAAQ,OAAO,OAAO;EAAM;CAAE;CACpE,IAAI,OAAO,eAAe,QACzB,OAAO;EAAE,OAAO;GAAE,GAAG;GAAM,eAAe,OAAO;EAAW;EAAG,OAAO,OAAO;CAAM;CAGpF,OAAO,KAAK,SAAS,MAAM,OAAO,UAAU;AAC7C"}
@@ -3,7 +3,7 @@ function bindKeyPress(root, signal) {
3
3
  let pressed;
4
4
  const release = () => {
5
5
  if (!pressed) return;
6
- delete pressed.dataset.sonicPressed;
6
+ delete pressed.dataset.sonicActive;
7
7
  pressed = void 0;
8
8
  };
9
9
  root.addEventListener("keydown", (event) => {
@@ -12,7 +12,7 @@ function bindKeyPress(root, signal) {
12
12
  if (!(part instanceof HTMLButtonElement) || part.disabled) return;
13
13
  release();
14
14
  pressed = part;
15
- part.dataset.sonicPressed = "";
15
+ part.dataset.sonicActive = "";
16
16
  }, { signal });
17
17
  root.addEventListener("keyup", (event) => {
18
18
  if (event.key === " " || event.key === "Meta") release();
@@ -1 +1 @@
1
- {"version":3,"file":"key-press.js","names":[],"sources":["../../../src/lib/key-press.ts"],"sourcesContent":["// Firefox never matches `:active` on a button that a key holds\nexport function bindKeyPress(root: HTMLElement, signal: AbortSignal): void {\n\tlet pressed: HTMLButtonElement | undefined;\n\tconst release = (): void => {\n\t\tif (!pressed) return;\n\n\t\tdelete pressed.dataset.sonicPressed;\n\t\tpressed = undefined;\n\t};\n\n\troot.addEventListener(\n\t\t'keydown',\n\t\t(event) => {\n\t\t\tif (event.key !== ' ' || event.repeat || event.defaultPrevented) return;\n\n\t\t\tconst part = event.target;\n\t\t\tif (!(part instanceof HTMLButtonElement) || part.disabled) return;\n\n\t\t\trelease();\n\t\t\tpressed = part;\n\t\t\tpart.dataset.sonicPressed = '';\n\t\t},\n\t\t{ signal },\n\t);\n\troot.addEventListener(\n\t\t'keyup',\n\t\t(event) => {\n\t\t\t// macOS sends no `keyup` for a key let go while Cmd is down\n\t\t\tif (event.key === ' ' || event.key === 'Meta') release();\n\t\t},\n\t\t{ signal },\n\t);\n\troot.addEventListener(\n\t\t'focusout',\n\t\t(event) => {\n\t\t\tif (event.target === pressed) release();\n\t\t},\n\t\t{ signal },\n\t);\n\tsignal.addEventListener('abort', release, { once: true });\n}\n"],"mappings":";AACA,SAAgB,aAAa,MAAmB,QAA2B;CAC1E,IAAI;CACJ,MAAM,gBAAsB;EAC3B,IAAI,CAAC,SAAS;EAEd,OAAO,QAAQ,QAAQ;EACvB,UAAU;CACX;CAEA,KAAK,iBACJ,YACC,UAAU;EACV,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU,MAAM,kBAAkB;EAEjE,MAAM,OAAO,MAAM;EACnB,IAAI,EAAE,gBAAgB,sBAAsB,KAAK,UAAU;EAE3D,QAAQ;EACR,UAAU;EACV,KAAK,QAAQ,eAAe;CAC7B,GACA,EAAE,OAAO,CACV;CACA,KAAK,iBACJ,UACC,UAAU;EAEV,IAAI,MAAM,QAAQ,OAAO,MAAM,QAAQ,QAAQ,QAAQ;CACxD,GACA,EAAE,OAAO,CACV;CACA,KAAK,iBACJ,aACC,UAAU;EACV,IAAI,MAAM,WAAW,SAAS,QAAQ;CACvC,GACA,EAAE,OAAO,CACV;CACA,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AACzD"}
1
+ {"version":3,"file":"key-press.js","names":[],"sources":["../../../src/lib/key-press.ts"],"sourcesContent":["// Firefox never matches `:active` on a button that a key holds\nexport function bindKeyPress(root: HTMLElement, signal: AbortSignal): void {\n\tlet pressed: HTMLButtonElement | undefined;\n\tconst release = (): void => {\n\t\tif (!pressed) return;\n\n\t\tdelete pressed.dataset.sonicActive;\n\t\tpressed = undefined;\n\t};\n\n\troot.addEventListener(\n\t\t'keydown',\n\t\t(event) => {\n\t\t\tif (event.key !== ' ' || event.repeat || event.defaultPrevented) return;\n\n\t\t\tconst part = event.target;\n\t\t\tif (!(part instanceof HTMLButtonElement) || part.disabled) return;\n\n\t\t\trelease();\n\t\t\tpressed = part;\n\t\t\tpart.dataset.sonicActive = '';\n\t\t},\n\t\t{ signal },\n\t);\n\troot.addEventListener(\n\t\t'keyup',\n\t\t(event) => {\n\t\t\t// macOS sends no `keyup` for a key let go while Cmd is down\n\t\t\tif (event.key === ' ' || event.key === 'Meta') release();\n\t\t},\n\t\t{ signal },\n\t);\n\troot.addEventListener(\n\t\t'focusout',\n\t\t(event) => {\n\t\t\tif (event.target === pressed) release();\n\t\t},\n\t\t{ signal },\n\t);\n\tsignal.addEventListener('abort', release, { once: true });\n}\n"],"mappings":";AACA,SAAgB,aAAa,MAAmB,QAA2B;CAC1E,IAAI;CACJ,MAAM,gBAAsB;EAC3B,IAAI,CAAC,SAAS;EAEd,OAAO,QAAQ,QAAQ;EACvB,UAAU;CACX;CAEA,KAAK,iBACJ,YACC,UAAU;EACV,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU,MAAM,kBAAkB;EAEjE,MAAM,OAAO,MAAM;EACnB,IAAI,EAAE,gBAAgB,sBAAsB,KAAK,UAAU;EAE3D,QAAQ;EACR,UAAU;EACV,KAAK,QAAQ,cAAc;CAC5B,GACA,EAAE,OAAO,CACV;CACA,KAAK,iBACJ,UACC,UAAU;EAEV,IAAI,MAAM,QAAQ,OAAO,MAAM,QAAQ,QAAQ,QAAQ;CACxD,GACA,EAAE,OAAO,CACV;CACA,KAAK,iBACJ,aACC,UAAU;EACV,IAAI,MAAM,WAAW,SAAS,QAAQ;CACvC,GACA,EAAE,OAAO,CACV;CACA,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AACzD"}
@@ -0,0 +1,7 @@
1
+ //#region src/lib/marker-band.d.ts
2
+ export interface Point {
3
+ x: number;
4
+ y: number;
5
+ }
6
+ //#endregion
7
+ //# sourceMappingURL=marker-band.d.ts.map
@@ -2,7 +2,7 @@ import { readPxProperty } from "./read-px-property.js";
2
2
 
3
3
  //#region src/lib/marker-band.ts
4
4
  const kindPattern = /^[a-z][a-z0-9-]*$/;
5
- const snapSlopPx = 4;
5
+ const snapMarginPx = 4;
6
6
  function isKind(kind) {
7
7
  return kind !== void 0 && kindPattern.test(kind);
8
8
  }
@@ -13,45 +13,61 @@ function writeKind(part, kind, fallback) {
13
13
  }
14
14
  part.style.setProperty("--_sonic-marker", `var(--sonic-marker-${kind}, var(${fallback}))`);
15
15
  }
16
- function renderMarker(marker, [from, to], lane) {
16
+ function createPart(marker) {
17
17
  const part = document.createElement("div");
18
- part.className = lane === void 0 ? "sonic-wavestrip-region" : "sonic-wavestrip-marker";
19
- if (lane !== void 0) part.style.setProperty("--_sonic-marker-lane", String(lane));
20
- part.style.setProperty("--_sonic-marker-from", String(from));
21
- part.style.setProperty("--_sonic-marker-to", String(to));
18
+ part.className = marker.end === void 0 ? "sonic-wavestrip-marker" : "sonic-wavestrip-region";
22
19
  writeKind(part, marker.kind, "--_sonic-marker-default");
23
20
  if (marker.dimmed === true) part.dataset.sonicDimmed = "";
24
21
  return part;
25
22
  }
26
23
  function createMarkerBand(band) {
27
- let dots = [];
24
+ let built;
25
+ let placed = [];
28
26
  const px = (property) => readPxProperty(getComputedStyle(band), property, 0);
27
+ function build(markers, draw) {
28
+ const parts = markers.map((marker) => {
29
+ const part = createPart(marker);
30
+ if (draw && marker.end === void 0) {
31
+ part.style.setProperty("--_sonic-marker-paint", "transparent");
32
+ draw(marker, part);
33
+ }
34
+ return part;
35
+ });
36
+ band.replaceChildren(...parts);
37
+ built = {
38
+ draw,
39
+ markers,
40
+ parts
41
+ };
42
+ return parts;
43
+ }
29
44
  return {
30
- at: ({ x, y }, widthPx) => {
45
+ inReach: (point, widthPx) => {
31
46
  const size = px("--_sonic-marker-size");
32
47
  const step = px("--_sonic-marker-step");
33
- const nearest = nearestDot({
34
- x,
35
- y
36
- }, dots.map(({ at, lane }) => ({
48
+ return centersInReach(point, placed.map(({ at, lane }) => ({
37
49
  x: at * widthPx,
38
50
  y: size / 2 + lane * step
39
- })), size / 2 + snapSlopPx);
40
- return nearest === void 0 ? void 0 : dots[nearest]?.index;
51
+ })), size / 2 + snapMarginPx).flatMap((order) => placed[order]?.index ?? []);
41
52
  },
42
- render: (markers, proportions) => {
53
+ render: (markers, proportions, draw) => {
43
54
  const widthPx = band.getBoundingClientRect().width;
44
55
  const points = markers.flatMap((marker, index) => marker.end === void 0 ? [{
45
56
  at: proportions[index]?.[0] ?? 0,
46
57
  index
47
58
  }] : []);
48
59
  const lanes = markerLanes(points.map(({ at }) => at * widthPx), px("--_sonic-marker-step"));
49
- dots = points.map((point, order) => ({
60
+ const parts = built?.markers === markers && built.draw === draw ? built.parts : build(markers, draw);
61
+ placed = points.map((point, order) => ({
50
62
  ...point,
51
63
  lane: lanes[order] ?? 0
52
64
  }));
53
- const laneOf = new Map(dots.map(({ index, lane }) => [index, lane]));
54
- band.replaceChildren(...markers.map((marker, index) => renderMarker(marker, proportions[index] ?? [0, 0], laneOf.get(index))));
65
+ for (const [index, part] of parts.entries()) {
66
+ const [from, to] = proportions[index] ?? [0, 0];
67
+ part.style.setProperty("--_sonic-marker-from", String(from));
68
+ part.style.setProperty("--_sonic-marker-to", String(to));
69
+ }
70
+ for (const { index, lane } of placed) parts[index]?.style.setProperty("--_sonic-marker-lane", String(lane));
55
71
  }
56
72
  };
57
73
  }
@@ -64,18 +80,13 @@ function markerLanes(positions, minDistance) {
64
80
  return lane;
65
81
  });
66
82
  }
67
- function nearestDot(point, dots, reach) {
68
- let nearest;
69
- let nearestDistance = reach * reach;
70
- for (const [index, dot] of dots.entries()) {
71
- const distance = (dot.x - point.x) ** 2 + (dot.y - point.y) ** 2;
72
- if (distance > nearestDistance) continue;
73
- nearest = index;
74
- nearestDistance = distance;
75
- }
76
- return nearest;
83
+ function centersInReach(point, centers, reach) {
84
+ return centers.map((center, index) => ({
85
+ distance: (center.x - point.x) ** 2 + (center.y - point.y) ** 2,
86
+ index
87
+ })).filter(({ distance }) => distance <= reach * reach).toSorted((first, second) => first.distance - second.distance || second.index - first.index).map(({ index }) => index);
77
88
  }
78
89
 
79
90
  //#endregion
80
- export { createMarkerBand, isKind, markerLanes, nearestDot, writeKind };
91
+ export { centersInReach, createMarkerBand, isKind, markerLanes, writeKind };
81
92
  //# sourceMappingURL=marker-band.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"marker-band.js","names":[],"sources":["../../../src/lib/marker-band.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ninterface MarkerLook {\n\tdimmed?: boolean;\n\tend?: number;\n\tkind?: string;\n}\n\ninterface Point {\n\tx: number;\n\ty: number;\n}\n\ninterface MarkerBand {\n\tat: (point: Point, widthPx: number) => number | undefined;\n\trender: (\n\t\tmarkers: ReadonlyArray<MarkerLook>,\n\t\tproportions: ReadonlyArray<[number, number]>,\n\t) => void;\n}\n\n// Interpolated into a style, so anything else falls back to the default colour\nconst kindPattern = /^[a-z][a-z0-9-]*$/;\n\nconst snapSlopPx = 4;\n\nexport function isKind(kind: string | undefined): kind is string {\n\treturn kind !== undefined && kindPattern.test(kind);\n}\n\nexport function writeKind(part: HTMLElement, kind: string | undefined, fallback: string): void {\n\tif (!isKind(kind)) {\n\t\tpart.style.removeProperty('--_sonic-marker');\n\t\treturn;\n\t}\n\n\tpart.style.setProperty('--_sonic-marker', `var(--sonic-marker-${kind}, var(${fallback}))`);\n}\n\nfunction renderMarker(\n\tmarker: MarkerLook,\n\t[from, to]: [number, number],\n\tlane: number | undefined,\n): HTMLElement {\n\tconst part = document.createElement('div');\n\n\tpart.className = lane === undefined ? 'sonic-wavestrip-region' : 'sonic-wavestrip-marker';\n\tif (lane !== undefined) part.style.setProperty('--_sonic-marker-lane', String(lane));\n\tpart.style.setProperty('--_sonic-marker-from', String(from));\n\tpart.style.setProperty('--_sonic-marker-to', String(to));\n\twriteKind(part, marker.kind, '--_sonic-marker-default');\n\tif (marker.dimmed === true) part.dataset.sonicDimmed = '';\n\n\treturn part;\n}\n\nexport function createMarkerBand(band: HTMLElement): MarkerBand {\n\tlet dots: Array<{ at: number; index: number; lane: number }> = [];\n\n\tconst px = (property: string): number => readPxProperty(getComputedStyle(band), property, 0);\n\n\treturn {\n\t\tat: ({ x, y }, widthPx) => {\n\t\t\tconst size = px('--_sonic-marker-size');\n\t\t\tconst step = px('--_sonic-marker-step');\n\t\t\tconst nearest = nearestDot(\n\t\t\t\t{ x, y },\n\t\t\t\tdots.map(({ at, lane }) => ({ x: at * widthPx, y: size / 2 + lane * step })),\n\t\t\t\tsize / 2 + snapSlopPx,\n\t\t\t);\n\n\t\t\treturn nearest === undefined ? undefined : dots[nearest]?.index;\n\t\t},\n\t\trender: (markers, proportions) => {\n\t\t\tconst widthPx = band.getBoundingClientRect().width;\n\t\t\tconst points = markers.flatMap((marker, index) =>\n\t\t\t\tmarker.end === undefined ? [{ at: proportions[index]?.[0] ?? 0, index }] : [],\n\t\t\t);\n\t\t\tconst lanes = markerLanes(\n\t\t\t\tpoints.map(({ at }) => at * widthPx),\n\t\t\t\tpx('--_sonic-marker-step'),\n\t\t\t);\n\n\t\t\tdots = points.map((point, order) => ({ ...point, lane: lanes[order] ?? 0 }));\n\n\t\t\tconst laneOf = new Map(dots.map(({ index, lane }) => [index, lane]));\n\n\t\t\tband.replaceChildren(\n\t\t\t\t...markers.map((marker, index) =>\n\t\t\t\t\trenderMarker(marker, proportions[index] ?? [0, 0], laneOf.get(index)),\n\t\t\t\t),\n\t\t\t);\n\t\t},\n\t};\n}\n\nexport function markerLanes(positions: ReadonlyArray<number>, minDistance: number): Array<number> {\n\tconst lastInLane: Array<number> = [];\n\n\treturn positions.map((position) => {\n\t\tconst free = lastInLane.findIndex((last) => position - last >= minDistance);\n\t\tconst lane = free === -1 ? lastInLane.length : free;\n\n\t\tlastInLane[lane] = position;\n\n\t\treturn lane;\n\t});\n}\n\nexport function nearestDot(\n\tpoint: Point,\n\tdots: ReadonlyArray<Point>,\n\treach: number,\n): number | undefined {\n\tlet nearest: number | undefined;\n\tlet nearestDistance = reach * reach;\n\n\tfor (const [index, dot] of dots.entries()) {\n\t\tconst distance = (dot.x - point.x) ** 2 + (dot.y - point.y) ** 2;\n\t\tif (distance > nearestDistance) continue;\n\n\t\tnearest = index;\n\t\tnearestDistance = distance;\n\t}\n\n\treturn nearest;\n}\n"],"mappings":";;;AAsBA,MAAM,cAAc;AAEpB,MAAM,aAAa;AAEnB,SAAgB,OAAO,MAA0C;CAChE,OAAO,SAAS,UAAa,YAAY,KAAK,IAAI;AACnD;AAEA,SAAgB,UAAU,MAAmB,MAA0B,UAAwB;CAC9F,IAAI,CAAC,OAAO,IAAI,GAAG;EAClB,KAAK,MAAM,eAAe,iBAAiB;EAC3C;CACD;CAEA,KAAK,MAAM,YAAY,mBAAmB,sBAAsB,KAAK,QAAQ,SAAS,GAAG;AAC1F;AAEA,SAAS,aACR,QACA,CAAC,MAAM,KACP,MACc;CACd,MAAM,OAAO,SAAS,cAAc,KAAK;CAEzC,KAAK,YAAY,SAAS,SAAY,2BAA2B;CACjE,IAAI,SAAS,QAAW,KAAK,MAAM,YAAY,wBAAwB,OAAO,IAAI,CAAC;CACnF,KAAK,MAAM,YAAY,wBAAwB,OAAO,IAAI,CAAC;CAC3D,KAAK,MAAM,YAAY,sBAAsB,OAAO,EAAE,CAAC;CACvD,UAAU,MAAM,OAAO,MAAM,yBAAyB;CACtD,IAAI,OAAO,WAAW,MAAM,KAAK,QAAQ,cAAc;CAEvD,OAAO;AACR;AAEA,SAAgB,iBAAiB,MAA+B;CAC/D,IAAI,OAA2D,CAAC;CAEhE,MAAM,MAAM,aAA6B,eAAe,iBAAiB,IAAI,GAAG,UAAU,CAAC;CAE3F,OAAO;EACN,KAAK,EAAE,GAAG,KAAK,YAAY;GAC1B,MAAM,OAAO,GAAG,sBAAsB;GACtC,MAAM,OAAO,GAAG,sBAAsB;GACtC,MAAM,UAAU,WACf;IAAE;IAAG;GAAE,GACP,KAAK,KAAK,EAAE,IAAI,YAAY;IAAE,GAAG,KAAK;IAAS,GAAG,OAAO,IAAI,OAAO;GAAK,EAAE,GAC3E,OAAO,IAAI,UACZ;GAEA,OAAO,YAAY,SAAY,SAAY,KAAK,QAAQ,EAAE;EAC3D;EACA,SAAS,SAAS,gBAAgB;GACjC,MAAM,UAAU,KAAK,sBAAsB,CAAC,CAAC;GAC7C,MAAM,SAAS,QAAQ,SAAS,QAAQ,UACvC,OAAO,QAAQ,SAAY,CAAC;IAAE,IAAI,YAAY,MAAM,GAAG,MAAM;IAAG;GAAM,CAAC,IAAI,CAAC,CAC7E;GACA,MAAM,QAAQ,YACb,OAAO,KAAK,EAAE,SAAS,KAAK,OAAO,GACnC,GAAG,sBAAsB,CAC1B;GAEA,OAAO,OAAO,KAAK,OAAO,WAAW;IAAE,GAAG;IAAO,MAAM,MAAM,UAAU;GAAE,EAAE;GAE3E,MAAM,SAAS,IAAI,IAAI,KAAK,KAAK,EAAE,OAAO,WAAW,CAAC,OAAO,IAAI,CAAC,CAAC;GAEnE,KAAK,gBACJ,GAAG,QAAQ,KAAK,QAAQ,UACvB,aAAa,QAAQ,YAAY,UAAU,CAAC,GAAG,CAAC,GAAG,OAAO,IAAI,KAAK,CAAC,CACrE,CACD;EACD;CACD;AACD;AAEA,SAAgB,YAAY,WAAkC,aAAoC;CACjG,MAAM,aAA4B,CAAC;CAEnC,OAAO,UAAU,KAAK,aAAa;EAClC,MAAM,OAAO,WAAW,WAAW,SAAS,WAAW,QAAQ,WAAW;EAC1E,MAAM,OAAO,SAAS,KAAK,WAAW,SAAS;EAE/C,WAAW,QAAQ;EAEnB,OAAO;CACR,CAAC;AACF;AAEA,SAAgB,WACf,OACA,MACA,OACqB;CACrB,IAAI;CACJ,IAAI,kBAAkB,QAAQ;CAE9B,KAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,QAAQ,GAAG;EAC1C,MAAM,YAAY,IAAI,IAAI,MAAM,MAAM,KAAK,IAAI,IAAI,MAAM,MAAM;EAC/D,IAAI,WAAW,iBAAiB;EAEhC,UAAU;EACV,kBAAkB;CACnB;CAEA,OAAO;AACR"}
1
+ {"version":3,"file":"marker-band.js","names":[],"sources":["../../../src/lib/marker-band.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ninterface MarkerLook {\n\tdimmed?: boolean;\n\tend?: number;\n\tkind?: string;\n}\n\nexport interface Point {\n\tx: number;\n\ty: number;\n}\n\ninterface MarkerBand<Marker extends MarkerLook> {\n\tinReach: (point: Point, widthPx: number) => Array<number>;\n\trender: (\n\t\tmarkers: ReadonlyArray<Marker>,\n\t\tproportions: ReadonlyArray<[number, number]>,\n\t\tdraw?: (marker: Marker, part: HTMLElement) => void,\n\t) => void;\n}\n\n// Interpolated into a style, so anything else falls back to the default colour\nconst kindPattern = /^[a-z][a-z0-9-]*$/;\n\nconst snapMarginPx = 4;\n\nexport function isKind(kind: string | undefined): kind is string {\n\treturn kind !== undefined && kindPattern.test(kind);\n}\n\nexport function writeKind(part: HTMLElement, kind: string | undefined, fallback: string): void {\n\tif (!isKind(kind)) {\n\t\tpart.style.removeProperty('--_sonic-marker');\n\t\treturn;\n\t}\n\n\tpart.style.setProperty('--_sonic-marker', `var(--sonic-marker-${kind}, var(${fallback}))`);\n}\n\nfunction createPart(marker: MarkerLook): HTMLElement {\n\tconst part = document.createElement('div');\n\n\tpart.className = marker.end === undefined ? 'sonic-wavestrip-marker' : 'sonic-wavestrip-region';\n\twriteKind(part, marker.kind, '--_sonic-marker-default');\n\tif (marker.dimmed === true) part.dataset.sonicDimmed = '';\n\n\treturn part;\n}\n\nexport function createMarkerBand<Marker extends MarkerLook>(band: HTMLElement): MarkerBand<Marker> {\n\ttype Draw = (marker: Marker, part: HTMLElement) => void;\n\n\tlet built:\n\t\t| undefined\n\t\t| { draw: Draw | undefined; markers: ReadonlyArray<Marker>; parts: Array<HTMLElement> };\n\tlet placed: Array<{ at: number; index: number; lane: number }> = [];\n\n\tconst px = (property: string): number => readPxProperty(getComputedStyle(band), property, 0);\n\n\tfunction build(markers: ReadonlyArray<Marker>, draw: Draw | undefined): Array<HTMLElement> {\n\t\tconst parts = markers.map((marker) => {\n\t\t\tconst part = createPart(marker);\n\n\t\t\tif (draw && marker.end === undefined) {\n\t\t\t\tpart.style.setProperty('--_sonic-marker-paint', 'transparent');\n\t\t\t\tdraw(marker, part);\n\t\t\t}\n\n\t\t\treturn part;\n\t\t});\n\n\t\tband.replaceChildren(...parts);\n\t\tbuilt = { draw, markers, parts };\n\n\t\treturn parts;\n\t}\n\n\treturn {\n\t\tinReach: (point, widthPx) => {\n\t\t\tconst size = px('--_sonic-marker-size');\n\t\t\tconst step = px('--_sonic-marker-step');\n\n\t\t\treturn centersInReach(\n\t\t\t\tpoint,\n\t\t\t\tplaced.map(({ at, lane }) => ({ x: at * widthPx, y: size / 2 + lane * step })),\n\t\t\t\tsize / 2 + snapMarginPx,\n\t\t\t).flatMap((order) => placed[order]?.index ?? []);\n\t\t},\n\t\trender: (markers, proportions, draw) => {\n\t\t\tconst widthPx = band.getBoundingClientRect().width;\n\t\t\tconst points = markers.flatMap((marker, index) =>\n\t\t\t\tmarker.end === undefined ? [{ at: proportions[index]?.[0] ?? 0, index }] : [],\n\t\t\t);\n\t\t\tconst lanes = markerLanes(\n\t\t\t\tpoints.map(({ at }) => at * widthPx),\n\t\t\t\tpx('--_sonic-marker-step'),\n\t\t\t);\n\t\t\t// A `markers` write always makes a new array, so the same array and renderer mean no write\n\t\t\tconst parts =\n\t\t\t\tbuilt?.markers === markers && built.draw === draw ? built.parts : build(markers, draw);\n\n\t\t\tplaced = points.map((point, order) => ({ ...point, lane: lanes[order] ?? 0 }));\n\t\t\tfor (const [index, part] of parts.entries()) {\n\t\t\t\tconst [from, to] = proportions[index] ?? [0, 0];\n\n\t\t\t\tpart.style.setProperty('--_sonic-marker-from', String(from));\n\t\t\t\tpart.style.setProperty('--_sonic-marker-to', String(to));\n\t\t\t}\n\t\t\tfor (const { index, lane } of placed) {\n\t\t\t\tparts[index]?.style.setProperty('--_sonic-marker-lane', String(lane));\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport function markerLanes(positions: ReadonlyArray<number>, minDistance: number): Array<number> {\n\tconst lastInLane: Array<number> = [];\n\n\treturn positions.map((position) => {\n\t\tconst free = lastInLane.findIndex((last) => position - last >= minDistance);\n\t\tconst lane = free === -1 ? lastInLane.length : free;\n\n\t\tlastInLane[lane] = position;\n\n\t\treturn lane;\n\t});\n}\n\n// A later marker wins a tie, as it is drawn over the earlier one\nexport function centersInReach(\n\tpoint: Point,\n\tcenters: ReadonlyArray<Point>,\n\treach: number,\n): Array<number> {\n\treturn centers\n\t\t.map((center, index) => ({\n\t\t\tdistance: (center.x - point.x) ** 2 + (center.y - point.y) ** 2,\n\t\t\tindex,\n\t\t}))\n\t\t.filter(({ distance }) => distance <= reach * reach)\n\t\t.toSorted((first, second) => first.distance - second.distance || second.index - first.index)\n\t\t.map(({ index }) => index);\n}\n"],"mappings":";;;AAuBA,MAAM,cAAc;AAEpB,MAAM,eAAe;AAErB,SAAgB,OAAO,MAA0C;CAChE,OAAO,SAAS,UAAa,YAAY,KAAK,IAAI;AACnD;AAEA,SAAgB,UAAU,MAAmB,MAA0B,UAAwB;CAC9F,IAAI,CAAC,OAAO,IAAI,GAAG;EAClB,KAAK,MAAM,eAAe,iBAAiB;EAC3C;CACD;CAEA,KAAK,MAAM,YAAY,mBAAmB,sBAAsB,KAAK,QAAQ,SAAS,GAAG;AAC1F;AAEA,SAAS,WAAW,QAAiC;CACpD,MAAM,OAAO,SAAS,cAAc,KAAK;CAEzC,KAAK,YAAY,OAAO,QAAQ,SAAY,2BAA2B;CACvE,UAAU,MAAM,OAAO,MAAM,yBAAyB;CACtD,IAAI,OAAO,WAAW,MAAM,KAAK,QAAQ,cAAc;CAEvD,OAAO;AACR;AAEA,SAAgB,iBAA4C,MAAuC;CAGlG,IAAI;CAGJ,IAAI,SAA6D,CAAC;CAElE,MAAM,MAAM,aAA6B,eAAe,iBAAiB,IAAI,GAAG,UAAU,CAAC;CAE3F,SAAS,MAAM,SAAgC,MAA4C;EAC1F,MAAM,QAAQ,QAAQ,KAAK,WAAW;GACrC,MAAM,OAAO,WAAW,MAAM;GAE9B,IAAI,QAAQ,OAAO,QAAQ,QAAW;IACrC,KAAK,MAAM,YAAY,yBAAyB,aAAa;IAC7D,KAAK,QAAQ,IAAI;GAClB;GAEA,OAAO;EACR,CAAC;EAED,KAAK,gBAAgB,GAAG,KAAK;EAC7B,QAAQ;GAAE;GAAM;GAAS;EAAM;EAE/B,OAAO;CACR;CAEA,OAAO;EACN,UAAU,OAAO,YAAY;GAC5B,MAAM,OAAO,GAAG,sBAAsB;GACtC,MAAM,OAAO,GAAG,sBAAsB;GAEtC,OAAO,eACN,OACA,OAAO,KAAK,EAAE,IAAI,YAAY;IAAE,GAAG,KAAK;IAAS,GAAG,OAAO,IAAI,OAAO;GAAK,EAAE,GAC7E,OAAO,IAAI,YACZ,CAAC,CAAC,SAAS,UAAU,OAAO,MAAM,EAAE,SAAS,CAAC,CAAC;EAChD;EACA,SAAS,SAAS,aAAa,SAAS;GACvC,MAAM,UAAU,KAAK,sBAAsB,CAAC,CAAC;GAC7C,MAAM,SAAS,QAAQ,SAAS,QAAQ,UACvC,OAAO,QAAQ,SAAY,CAAC;IAAE,IAAI,YAAY,MAAM,GAAG,MAAM;IAAG;GAAM,CAAC,IAAI,CAAC,CAC7E;GACA,MAAM,QAAQ,YACb,OAAO,KAAK,EAAE,SAAS,KAAK,OAAO,GACnC,GAAG,sBAAsB,CAC1B;GAEA,MAAM,QACL,OAAO,YAAY,WAAW,MAAM,SAAS,OAAO,MAAM,QAAQ,MAAM,SAAS,IAAI;GAEtF,SAAS,OAAO,KAAK,OAAO,WAAW;IAAE,GAAG;IAAO,MAAM,MAAM,UAAU;GAAE,EAAE;GAC7E,KAAK,MAAM,CAAC,OAAO,SAAS,MAAM,QAAQ,GAAG;IAC5C,MAAM,CAAC,MAAM,MAAM,YAAY,UAAU,CAAC,GAAG,CAAC;IAE9C,KAAK,MAAM,YAAY,wBAAwB,OAAO,IAAI,CAAC;IAC3D,KAAK,MAAM,YAAY,sBAAsB,OAAO,EAAE,CAAC;GACxD;GACA,KAAK,MAAM,EAAE,OAAO,UAAU,QAC7B,MAAM,MAAM,EAAE,MAAM,YAAY,wBAAwB,OAAO,IAAI,CAAC;EAEtE;CACD;AACD;AAEA,SAAgB,YAAY,WAAkC,aAAoC;CACjG,MAAM,aAA4B,CAAC;CAEnC,OAAO,UAAU,KAAK,aAAa;EAClC,MAAM,OAAO,WAAW,WAAW,SAAS,WAAW,QAAQ,WAAW;EAC1E,MAAM,OAAO,SAAS,KAAK,WAAW,SAAS;EAE/C,WAAW,QAAQ;EAEnB,OAAO;CACR,CAAC;AACF;AAGA,SAAgB,eACf,OACA,SACA,OACgB;CAChB,OAAO,QACL,KAAK,QAAQ,WAAW;EACxB,WAAW,OAAO,IAAI,MAAM,MAAM,KAAK,OAAO,IAAI,MAAM,MAAM;EAC9D;CACD,EAAE,CAAC,CACF,QAAQ,EAAE,eAAe,YAAY,QAAQ,KAAK,CAAC,CACnD,UAAU,OAAO,WAAW,MAAM,WAAW,OAAO,YAAY,OAAO,QAAQ,MAAM,KAAK,CAAC,CAC3F,KAAK,EAAE,YAAY,KAAK;AAC3B"}
@@ -0,0 +1,49 @@
1
+ import { clamp, clampProportion, trimFloat } from "./math.js";
2
+ import { fineFactor } from "./drag-step.js";
3
+
4
+ //#region src/lib/region-drag.ts
5
+ function regionAt(bodies, x) {
6
+ const index = bodies.findLastIndex((body) => body !== void 0 && x >= body[0] && x <= body[1]);
7
+ return index === -1 ? void 0 : index;
8
+ }
9
+ function regionTravel({ end, start }, [low, high]) {
10
+ return [low, Math.max(low, trimFloat(high - (end - start)))];
11
+ }
12
+ function moveRegionTo(mapping, span, start) {
13
+ const moved = clamp(mapping.snap(start), ...regionTravel(span, mapping.bounds));
14
+ return {
15
+ end: trimFloat(moved + (span.end - span.start)),
16
+ start: moved
17
+ };
18
+ }
19
+ function startRegionDrag({ position, ...fixed }) {
20
+ return {
21
+ ...fixed,
22
+ isEngaged: false,
23
+ lastPosition: position,
24
+ startPosition: position,
25
+ travelPx: 0
26
+ };
27
+ }
28
+ function stepRegionDrag(mapping, state, move) {
29
+ if (!(state.isEngaged || Math.abs(move.position - state.startPosition) >= state.thresholdPx)) return {
30
+ span: void 0,
31
+ state
32
+ };
33
+ const pace = move.isFine ? fineFactor : 1;
34
+ const travelPx = state.travelPx + (move.position - state.lastPosition) * pace;
35
+ const at = mapping.proportionOf(state.from.start) + travelPx / state.widthPx;
36
+ return {
37
+ span: moveRegionTo(mapping, state.from, mapping.valueAt(clampProportion(at))),
38
+ state: {
39
+ ...state,
40
+ isEngaged: true,
41
+ lastPosition: move.position,
42
+ travelPx
43
+ }
44
+ };
45
+ }
46
+
47
+ //#endregion
48
+ export { moveRegionTo, regionAt, regionTravel, startRegionDrag, stepRegionDrag };
49
+ //# sourceMappingURL=region-drag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region-drag.js","names":[],"sources":["../../../src/lib/region-drag.ts"],"sourcesContent":["import type { ValueMapping } from '#lib/value-mapping.ts';\n\nimport { fineFactor } from '#lib/drag-step.ts';\nimport { clamp, clampProportion, trimFloat } from '#lib/math.ts';\n\nexport interface RegionSpan {\n\tend: number;\n\tstart: number;\n}\n\nexport interface RegionDragState {\n\tfrom: RegionSpan;\n\tisEngaged: boolean;\n\tlastPosition: number;\n\tstartPosition: number;\n\tthresholdPx: number;\n\ttravelPx: number;\n\twidthPx: number;\n}\n\ninterface RegionDragStart {\n\tfrom: RegionSpan;\n\tposition: number;\n\tthresholdPx: number;\n\twidthPx: number;\n}\n\ninterface RegionDragStep {\n\tspan: RegionSpan | undefined;\n\tstate: RegionDragState;\n}\n\n// A later body is drawn over an earlier one; `undefined` holds the place of one that takes no press\nexport function regionAt(\n\tbodies: ReadonlyArray<[number, number] | undefined>,\n\tx: number,\n): number | undefined {\n\tconst index = bodies.findLastIndex((body) => body !== undefined && x >= body[0] && x <= body[1]);\n\n\treturn index === -1 ? undefined : index;\n}\n\nexport function regionTravel(\n\t{ end, start }: RegionSpan,\n\t[low, high]: [number, number],\n): [number, number] {\n\treturn [low, Math.max(low, trimFloat(high - (end - start)))];\n}\n\nexport function moveRegionTo(mapping: ValueMapping, span: RegionSpan, start: number): RegionSpan {\n\tconst moved = clamp(mapping.snap(start), ...regionTravel(span, mapping.bounds));\n\n\treturn { end: trimFloat(moved + (span.end - span.start)), start: moved };\n}\n\nexport function startRegionDrag({ position, ...fixed }: RegionDragStart): RegionDragState {\n\treturn {\n\t\t...fixed,\n\t\tisEngaged: false,\n\t\tlastPosition: position,\n\t\tstartPosition: position,\n\t\ttravelPx: 0,\n\t};\n}\n\n// `span` stays `undefined` until the pointer has travelled the threshold, so a lift before then is a press\nexport function stepRegionDrag(\n\tmapping: ValueMapping,\n\tstate: RegionDragState,\n\tmove: { isFine: boolean; position: number },\n): RegionDragStep {\n\tconst isEngaged =\n\t\tstate.isEngaged || Math.abs(move.position - state.startPosition) >= state.thresholdPx;\n\tif (!isEngaged) return { span: undefined, state };\n\n\tconst pace = move.isFine ? fineFactor : 1;\n\tconst travelPx = state.travelPx + (move.position - state.lastPosition) * pace;\n\tconst at = mapping.proportionOf(state.from.start) + travelPx / state.widthPx;\n\n\treturn {\n\t\tspan: moveRegionTo(mapping, state.from, mapping.valueAt(clampProportion(at))),\n\t\tstate: { ...state, isEngaged: true, lastPosition: move.position, travelPx },\n\t};\n}\n"],"mappings":";;;;AAiCA,SAAgB,SACf,QACA,GACqB;CACrB,MAAM,QAAQ,OAAO,eAAe,SAAS,SAAS,UAAa,KAAK,KAAK,MAAM,KAAK,KAAK,EAAE;CAE/F,OAAO,UAAU,KAAK,SAAY;AACnC;AAEA,SAAgB,aACf,EAAE,KAAK,SACP,CAAC,KAAK,OACa;CACnB,OAAO,CAAC,KAAK,KAAK,IAAI,KAAK,UAAU,QAAQ,MAAM,MAAM,CAAC,CAAC;AAC5D;AAEA,SAAgB,aAAa,SAAuB,MAAkB,OAA2B;CAChG,MAAM,QAAQ,MAAM,QAAQ,KAAK,KAAK,GAAG,GAAG,aAAa,MAAM,QAAQ,MAAM,CAAC;CAE9E,OAAO;EAAE,KAAK,UAAU,SAAS,KAAK,MAAM,KAAK,MAAM;EAAG,OAAO;CAAM;AACxE;AAEA,SAAgB,gBAAgB,EAAE,UAAU,GAAG,SAA2C;CACzF,OAAO;EACN,GAAG;EACH,WAAW;EACX,cAAc;EACd,eAAe;EACf,UAAU;CACX;AACD;AAGA,SAAgB,eACf,SACA,OACA,MACiB;CAGjB,IAAI,EADH,MAAM,aAAa,KAAK,IAAI,KAAK,WAAW,MAAM,aAAa,KAAK,MAAM,cAC3D,OAAO;EAAE,MAAM;EAAW;CAAM;CAEhD,MAAM,OAAO,KAAK,SAAS,aAAa;CACxC,MAAM,WAAW,MAAM,YAAY,KAAK,WAAW,MAAM,gBAAgB;CACzE,MAAM,KAAK,QAAQ,aAAa,MAAM,KAAK,KAAK,IAAI,WAAW,MAAM;CAErE,OAAO;EACN,MAAM,aAAa,SAAS,MAAM,MAAM,QAAQ,QAAQ,gBAAgB,EAAE,CAAC,CAAC;EAC5E,OAAO;GAAE,GAAG;GAAO,WAAW;GAAM,cAAc,KAAK;GAAU;EAAS;CAC3E;AACD"}
@@ -85,7 +85,8 @@
85
85
  font: inherit;
86
86
  color: var(--_sonic-ink);
87
87
  cursor: pointer;
88
- user-select: none;
88
+ -webkit-user-select: none;
89
+ user-select: none;
89
90
  background: var(--_sonic-well-floor);
90
91
  border: 0;
91
92
  border-radius: var(--_sonic-button-corner);
@@ -129,7 +130,7 @@
129
130
  .sonic-button:is(
130
131
  [aria-pressed='true'],
131
132
  [aria-expanded='true'],
132
- :where([momentary]:state(pressed)) > *
133
+ :where([momentary]) > [data-sonic-pressed]
133
134
  ) {
134
135
  --_sonic-keycap-shift: calc(var(--_sonic-keycap-depth) / 2);
135
136
  --_sonic-keycap-scale: var(--_sonic-button-latched-scale);
@@ -157,7 +158,7 @@
157
158
  );
158
159
  }
159
160
 
160
- .sonic-button:is([aria-pressed='true'],[aria-expanded='true'],:where([momentary]:state(pressed)) > *) .sonic-button-cap > [data-sonic-when='released'] {
161
+ .sonic-button:is([aria-pressed='true'],[aria-expanded='true'],:where([momentary]) > [data-sonic-pressed]) .sonic-button-cap > [data-sonic-when='released'] {
161
162
  display: none;
162
163
  }
163
164
 
@@ -166,19 +167,19 @@
166
167
  .sonic-button:is(
167
168
  [aria-pressed='true'],
168
169
  [aria-expanded='true'],
169
- :where([momentary]:state(pressed)) > *
170
+ :where([momentary]) > [data-sonic-pressed]
170
171
  ) {
171
172
  --_sonic-skirt: var(--_sonic-lit);
172
173
  --_sonic-face: var(--_sonic-lit);
173
174
  --_sonic-ink: HighlightText;
174
175
  }
175
176
 
176
- .sonic-button:is([aria-pressed='true'],[aria-expanded='true'],:where([momentary]:state(pressed)) > *):is(:disabled,[aria-disabled='true']) {
177
+ .sonic-button:is([aria-pressed='true'],[aria-expanded='true'],:where([momentary]) > [data-sonic-pressed]):is(:disabled,[aria-disabled='true']) {
177
178
  --_sonic-ink: ButtonFace;
178
179
  }
179
180
  }
180
181
 
181
- .sonic-button:is(:active, [data-sonic-pressed], :where([momentary]:state(pressed)) > *):where(
182
+ .sonic-button:is(:active, [data-sonic-active], :where([momentary]) > [data-sonic-pressed]):where(
182
183
  :not(:disabled, [aria-disabled='true'])
183
184
  ) {
184
185
  --_sonic-keycap-shift: calc(var(--_sonic-keycap-depth) / 2);
@@ -198,9 +199,10 @@
198
199
  [aria-expanded='true'],
199
200
  :disabled,
200
201
  [aria-disabled='true'],
201
- :where([momentary]:state(pressed), [armed]) > *,
202
+ :where([momentary]) > [data-sonic-pressed],
203
+ :where([armed]) > *,
202
204
  :where([busy]) > :not(:has(.sonic-led))
203
- ):is(:focus-visible, :active, [data-sonic-pressed]) {
205
+ ):is(:focus-visible, :active, [data-sonic-active]) {
204
206
  color: var(--_sonic-ink-lift);
205
207
  }
206
208
 
@@ -211,7 +213,8 @@
211
213
  [aria-expanded='true'],
212
214
  :disabled,
213
215
  [aria-disabled='true'],
214
- :where([momentary]:state(pressed), [armed]) > *,
216
+ :where([momentary]) > [data-sonic-pressed],
217
+ :where([armed]) > *,
215
218
  :where([busy]) > :not(:has(.sonic-led))
216
219
  ):hover {
217
220
  color: var(--_sonic-ink-lift);
@@ -301,12 +304,18 @@
301
304
  inline-size: auto;
302
305
  block-size: calc(var(--_sonic-button-size) * var(--_sonic-button-icon-ratio));
303
306
  aspect-ratio: auto 1;
304
- mask-image: linear-gradient(
307
+ -webkit-mask-image: linear-gradient(
305
308
  var(--_sonic-light-fall),
306
309
  #000 15%,
307
310
  rgb(0 0 0 / calc(1 - 0.4 * var(--_sonic-relief)))
308
311
  );
309
- mask-repeat: no-repeat;
312
+ mask-image: linear-gradient(
313
+ var(--_sonic-light-fall),
314
+ #000 15%,
315
+ rgb(0 0 0 / calc(1 - 0.4 * var(--_sonic-relief)))
316
+ );
317
+ -webkit-mask-repeat: no-repeat;
318
+ mask-repeat: no-repeat;
310
319
  }
311
320
 
312
321
  .sonic-button-cap:has(> :not(.sonic-led)) {
@@ -323,7 +332,7 @@
323
332
  .sonic-button:not(
324
333
  [aria-pressed='true'],
325
334
  [aria-expanded='true'],
326
- :where([momentary]:state(pressed)) > *
335
+ :where([momentary]) > [data-sonic-pressed]
327
336
  )
328
337
  .sonic-button-cap
329
338
  > [data-sonic-when='pressed'] {
@@ -357,7 +366,8 @@
357
366
  @media (forced-colors: active) {
358
367
  .sonic-button-cap > :not(.sonic-led) {
359
368
  filter: none;
360
- mask: none;
369
+ -webkit-mask: none;
370
+ mask: none;
361
371
  }
362
372
  }
363
373
  }
@@ -8,6 +8,7 @@
8
8
  @import './meter.css';
9
9
  @import './number.css';
10
10
  @import './panel.css';
11
+ @import './region.css';
11
12
  @import './ring.css';
12
13
  @import './screen.css';
13
14
  @import './segmented.css';