@xsynaptic/sonic-ui 0.16.0 → 0.18.0

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