@xsynaptic/sonic-ui 0.10.3 → 0.12.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 (68) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +12 -0
  3. package/dist/default/define/toggle.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/button.js +2 -0
  6. package/dist/default/elements/position-group.js +163 -0
  7. package/dist/default/elements/radio-group.js +34 -12
  8. package/dist/default/elements/sonic-element.js +11 -2
  9. package/dist/default/elements/switch.js +34 -168
  10. package/dist/default/elements/toggle.js +189 -0
  11. package/dist/default/elements/value-element.js +9 -0
  12. package/dist/default/elements/waveform.js +38 -19
  13. package/dist/default/elements/wavestrip.js +6 -1
  14. package/dist/default/index.js +2 -1
  15. package/dist/default/lib/key-press.js +27 -0
  16. package/dist/default/lib/strip-scene.js +4 -3
  17. package/dist/default/lib/toggle-travel.js +19 -0
  18. package/dist/dev/define/toggle.d.ts +2 -0
  19. package/dist/dev/define/toggle.js +8 -0
  20. package/dist/dev/define/toggle.js.map +1 -0
  21. package/dist/dev/define.d.ts +3 -1
  22. package/dist/dev/define.js +1 -0
  23. package/dist/dev/elements/button.js +2 -0
  24. package/dist/dev/elements/button.js.map +1 -1
  25. package/dist/dev/elements/position-group.d.ts +38 -0
  26. package/dist/dev/elements/position-group.js +164 -0
  27. package/dist/dev/elements/position-group.js.map +1 -0
  28. package/dist/dev/elements/radio-group.d.ts +4 -0
  29. package/dist/dev/elements/radio-group.js +34 -12
  30. package/dist/dev/elements/radio-group.js.map +1 -1
  31. package/dist/dev/elements/sonic-element.d.ts +1 -0
  32. package/dist/dev/elements/sonic-element.js +11 -2
  33. package/dist/dev/elements/sonic-element.js.map +1 -1
  34. package/dist/dev/elements/switch.d.ts +4 -19
  35. package/dist/dev/elements/switch.js +34 -168
  36. package/dist/dev/elements/switch.js.map +1 -1
  37. package/dist/dev/elements/toggle.d.ts +21 -0
  38. package/dist/dev/elements/toggle.js +197 -0
  39. package/dist/dev/elements/toggle.js.map +1 -0
  40. package/dist/dev/elements/value-element.d.ts +3 -0
  41. package/dist/dev/elements/value-element.js +9 -0
  42. package/dist/dev/elements/value-element.js.map +1 -1
  43. package/dist/dev/elements/waveform.d.ts +1 -1
  44. package/dist/dev/elements/waveform.js +38 -19
  45. package/dist/dev/elements/waveform.js.map +1 -1
  46. package/dist/dev/elements/wavestrip.d.ts +3 -0
  47. package/dist/dev/elements/wavestrip.js +6 -1
  48. package/dist/dev/elements/wavestrip.js.map +1 -1
  49. package/dist/dev/index.d.ts +3 -2
  50. package/dist/dev/index.js +2 -1
  51. package/dist/dev/lib/key-press.js +28 -0
  52. package/dist/dev/lib/key-press.js.map +1 -0
  53. package/dist/dev/lib/strip-scene.js +4 -3
  54. package/dist/dev/lib/strip-scene.js.map +1 -1
  55. package/dist/dev/lib/toggle-travel.js +20 -0
  56. package/dist/dev/lib/toggle-travel.js.map +1 -0
  57. package/dist/styles/button.css +2 -2
  58. package/dist/styles/controls.css +1 -0
  59. package/dist/styles/led.css +16 -13
  60. package/dist/styles/material.css +36 -9
  61. package/dist/styles/meter.css +7 -6
  62. package/dist/styles/segmented.css +8 -4
  63. package/dist/styles/spectrum.css +2 -2
  64. package/dist/styles/switch.css +6 -2
  65. package/dist/styles/toggle.css +481 -0
  66. package/dist/styles/waveform.css +5 -2
  67. package/dist/styles/wavestrip.css +7 -0
  68. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"waveform.js","names":[],"sources":["../../../src/elements/waveform.ts"],"sourcesContent":["import type { ValueAxis } from '#elements/value-gestures.ts';\nimport type { SurfaceFrame, SurfaceLook } from '#lib/canvas-surface.ts';\nimport type { Drawn, View } from '#lib/frame-timeline.ts';\nimport type { TimeRegions } from '#lib/time-regions.ts';\nimport type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nimport { SonicWaveElement } from '#elements/wave-element.ts';\nimport { frameTimeline } from '#lib/frame-timeline.ts';\nimport { isKind, writeKind } from '#lib/marker-band.ts';\nimport { createLabelRider } from '#lib/marker-rider.ts';\nimport { clamp } from '#lib/math.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { dueRegions, readRegions } from '#lib/time-regions.ts';\nimport { createTrackingClock } from '#lib/tracking-clock.ts';\nimport { waveformBuckets } from '#lib/waveform-buckets.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-waveform': SonicWaveform;\n\t}\n}\n\ntype Colour = keyof typeof colours;\n\ntype PeaksRequest = (fromSeconds: number, toSeconds: number) => unknown;\n\ninterface Scene extends View {\n\tcolours: SurfaceLook<Colour>['colours'];\n}\n\nconst colours = {\n\tends: '--_sonic-waveform-ends',\n\tgrid: '--_sonic-waveform-grid',\n\tmarker: '--_sonic-waveform-marker',\n\tplaceholder: '--_sonic-waveform-placeholder',\n\twave: '--_sonic-waveform-wave',\n\twaveEdge: '--_sonic-waveform-wave-edge',\n} as const;\n\nconst defaultZoom = 70;\n\nconst amplitudeMargin = 0.94;\n\nconst hatchPitchPx = 8;\nconst placeholderWavelengthPx = 48;\nconst placeholderAmplitude = 0.2;\n\nconst renderWaveform = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-waveform\" role=\"slider\" tabindex=\"0\">\n\t\t\t<canvas class=\"sonic-waveform-canvas\" aria-hidden=\"true\"></canvas>\n\t\t\t<div class=\"sonic-waveform-scrim\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-waveform-ghost\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-playhead\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-waveform-label\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-label\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-waveform-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nfunction placeLine(line: HTMLElement, at: number | undefined): void {\n\tconst isHidden = at === undefined || at < 0 || at > 1;\n\n\tif (line.hidden !== isHidden) line.hidden = isHidden;\n\tif (isHidden) return;\n\n\tconst translate = `${String(at * 100)}cqi`;\n\n\tif (line.style.getPropertyValue('translate') !== translate) {\n\t\tline.style.setProperty('translate', translate);\n\t}\n}\n\nfunction paintHatch(\n\tcontext: CanvasRenderingContext2D,\n\tscene: Scene,\n\t[from, to]: [number, number],\n): void {\n\tif (to <= from) return;\n\n\tconst { dpr, height } = scene;\n\tconst pitch = Math.max(2, Math.round(hatchPitchPx * dpr));\n\tconst offset = scene.startSeconds * scene.pixelsPerSecond;\n\n\tcontext.save();\n\tcontext.beginPath();\n\tcontext.rect(from, 0, to - from, height);\n\tcontext.clip();\n\tcontext.beginPath();\n\tfor (let x = Math.floor((from - height + offset) / pitch) * pitch - offset; x < to; x += pitch) {\n\t\tcontext.moveTo(x, height);\n\t\tcontext.lineTo(x + height, 0);\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.ends;\n\tcontext.stroke();\n\tcontext.restore();\n}\n\nfunction paintPlaceholders(context: CanvasRenderingContext2D, scene: Scene): void {\n\tif (scene.pending.length === 0) return;\n\n\tconst { dpr, height, pixelsPerSecond, startSeconds } = scene;\n\tconst wavelength = placeholderWavelengthPx * dpr;\n\tconst centre = height / 2;\n\tconst origin = startSeconds * pixelsPerSecond;\n\tconst waveY = (x: number): number =>\n\t\tcentre -\n\t\tMath.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) *\n\t\t\tcentre *\n\t\t\tplaceholderAmplitude;\n\n\tcontext.beginPath();\n\tfor (const [from, to] of scene.pending) {\n\t\tconst opening = (from - startSeconds) * pixelsPerSecond;\n\t\tconst closing = (to - startSeconds) * pixelsPerSecond;\n\n\t\tcontext.moveTo(opening, waveY(opening));\n\t\tfor (let x = opening + dpr; x < closing; x += dpr) context.lineTo(x, waveY(x));\n\t\tcontext.lineTo(closing, waveY(closing));\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.placeholder;\n\tcontext.stroke();\n}\n\nfunction waveFill(context: CanvasRenderingContext2D, scene: Scene): CanvasGradient {\n\tconst { wave, waveEdge } = scene.colours;\n\tconst gradient = context.createLinearGradient(0, 0, 0, scene.height);\n\n\tgradient.addColorStop(0, waveEdge);\n\tgradient.addColorStop(0.5, wave);\n\tgradient.addColorStop(1, waveEdge);\n\n\treturn gradient;\n}\n\nfunction paintWave(context: CanvasRenderingContext2D, scene: Scene, peaks: WaveformPeaks): void {\n\tconst buckets = waveformBuckets(peaks, scene);\n\tconst [first] = buckets;\n\tif (!first) return;\n\n\tconst centre = scene.height / 2;\n\tconst amplitudePx = centre * amplitudeMargin;\n\n\tcontext.fillStyle = waveFill(context, scene);\n\tcontext.beginPath();\n\tcontext.moveTo(first.x, centre);\n\tfor (const bucket of buckets) context.lineTo(bucket.x, centre - bucket.high * amplitudePx);\n\tfor (const bucket of buckets.toReversed())\n\t\tcontext.lineTo(bucket.x, centre - bucket.low * amplitudePx);\n\tcontext.closePath();\n\tcontext.fill();\n}\n\nexport class SonicWaveform extends SonicWaveElement<Colour> {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicWaveElement.observedAttributes,\n\t\t'pending-delay',\n\t\t'playing',\n\t\t'playback-rate',\n\t\t'reduced-motion',\n\t\t'zoom',\n\t];\n\n\tget peaks(): undefined | WaveformPeaks {\n\t\treturn this.#peaks;\n\t}\n\n\tset peaks(peaks: undefined | WaveformPeaks) {\n\t\tthis.#peaks = peaks;\n\t\tthis.renderEmpty();\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tget pending(): Array<[number, number]> {\n\t\treturn this.#pending.map(([start, end]) => [start, end]);\n\t}\n\n\tset pending(regions: TimeRegions | undefined) {\n\t\tconst listedMs = this.#pendingListedMs;\n\t\tconst nowMs = performance.now();\n\n\t\tthis.#pending = regions ? readRegions(regions) : [];\n\t\tthis.#pendingListedMs = new Map(\n\t\t\tthis.#pending.map((region) => {\n\t\t\t\tconst key = region.join(':');\n\n\t\t\t\treturn [key, listedMs.get(key) ?? nowMs];\n\t\t\t}),\n\t\t);\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tget pendingDelay(): number {\n\t\treturn Math.max(0, this.numberAttribute('pending-delay', 0));\n\t}\n\n\tset pendingDelay(value: number | undefined) {\n\t\tthis.reflect('pending-delay', value);\n\t}\n\n\tget playbackRate(): number {\n\t\tconst playbackRate = this.numberAttribute('playback-rate', 1);\n\n\t\treturn playbackRate > 0 ? playbackRate : 1;\n\t}\n\n\tset playbackRate(value: number | undefined) {\n\t\tthis.reflect('playback-rate', value);\n\t}\n\n\tget playing(): boolean {\n\t\treturn this.hasAttribute('playing');\n\t}\n\n\tset playing(isPlaying: boolean) {\n\t\tthis.reflect('playing', isPlaying);\n\t}\n\n\tget readTime(): (() => number | undefined) | undefined {\n\t\treturn this.#readTime;\n\t}\n\n\tset readTime(read: (() => number | undefined) | undefined) {\n\t\tthis.#readTime = read;\n\t\tthis.render();\n\t}\n\n\tget reducedMotion(): 'page' | 'scroll' {\n\t\treturn this.getAttribute('reduced-motion') === 'scroll' ? 'scroll' : 'page';\n\t}\n\n\tset reducedMotion(motion: 'page' | 'scroll' | undefined) {\n\t\tthis.reflect('reduced-motion', motion);\n\t}\n\n\tget requestPeaks(): PeaksRequest | undefined {\n\t\treturn this.#requestPeaks;\n\t}\n\n\tset requestPeaks(request: PeaksRequest | undefined) {\n\t\tthis.#requestPeaks = request;\n\t\tthis.#asked = undefined;\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tget zoom(): number {\n\t\tconst zoom = this.numberAttribute('zoom', defaultZoom);\n\n\t\treturn zoom > 0 ? zoom : defaultZoom;\n\t}\n\n\tset zoom(value: number | undefined) {\n\t\tthis.reflect('zoom', value);\n\t}\n\n\tprotected readonly control = renderWaveform();\n\n\tprotected readonly canvas = requireChild(\n\t\tthis.control,\n\t\t'.sonic-waveform-canvas',\n\t\tHTMLCanvasElement,\n\t);\n\n\tprotected readonly colours = colours;\n\n\tprotected readonly lengths = {};\n\n\tprotected readonly sheet = 'waveform.css';\n\n\tprotected readonly sizeProperty = '--_sonic-waveform-size';\n\n\t#asked: Promise<unknown> | undefined;\n\n\treadonly #clock = createTrackingClock();\n\n\t#drawn: Drawn | undefined;\n\n\treadonly #ghost = requireChild(this.control, '.sonic-waveform-ghost', HTMLDivElement);\n\n\t#grabbed: Drawn | undefined;\n\n\t#kindColours = new Map<string, string>();\n\n\t#paintedKey = '';\n\n\t#peaks: undefined | WaveformPeaks;\n\n\t#pending: Array<[number, number]> = [];\n\n\t#pendingListedMs = new Map<string, number>();\n\n\treadonly #playhead = requireChild(this.control, '.sonic-waveform-playhead', HTMLDivElement);\n\n\t#readTime: (() => number | undefined) | undefined;\n\n\t#requestPeaks: PeaksRequest | undefined;\n\n\treadonly #rider = createLabelRider(this.control, {\n\t\tclassName: 'sonic-waveform-label',\n\t\tcolourProperty: '--_sonic-waveform-marker',\n\t\tfadeProperty: '--_sonic-waveform-label-fade',\n\t\tinsetProperty: '--_sonic-waveform-label-inset',\n\t\tparkProperty: '--_sonic-waveform-label-park',\n\t});\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties(\n\t\t\t'peaks',\n\t\t\t'pending',\n\t\t\t'pendingDelay',\n\t\t\t'playing',\n\t\t\t'playbackRate',\n\t\t\t'readTime',\n\t\t\t'reducedMotion',\n\t\t\t'requestPeaks',\n\t\t\t'zoom',\n\t\t);\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tsuper.connect(signal);\n\t\tthis.bindGestures(this.control, signal, () => this.#grab());\n\t\tdocument.fonts.addEventListener(\n\t\t\t'loadingdone',\n\t\t\t() => {\n\t\t\t\tthis.renderMarkers();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\tprotected draw(): void {\n\t\tconst surface = this.surface();\n\n\t\tsurface?.requestFrame();\n\t\t// No frame follows the position out of view, or before the first one paints\n\t\tif (surface?.isVisible === false || !this.#drawn) this.showCurrentMarker(this.#sourceSeconds());\n\t}\n\n\tprotected isEmpty(): boolean {\n\t\treturn !this.#peaks || this.#peaks.samples.length === 0;\n\t}\n\n\tprotected paint(\n\t\tcontext: CanvasRenderingContext2D,\n\t\t{ frameMs, isDirty, isRebuilt, look, size }: SurfaceFrame<Colour>,\n\t): void {\n\t\tconst held = this.#held();\n\t\tconst pending = this.#duePending();\n\t\tconst timeline = frameTimeline({\n\t\t\tclockSeconds: this.#clock.read(frameMs, {\n\t\t\t\tisPlaying: this.playing,\n\t\t\t\tplaybackRate: this.playbackRate,\n\t\t\t\tseconds: this.#sourceSeconds(),\n\t\t\t}),\n\t\t\tframeMs,\n\t\t\t...(held ? { held } : {}),\n\t\t\tisPaged: look.isReducedMotion && this.reducedMotion === 'page',\n\t\t\tisPlaying: this.playing,\n\t\t\tisStill: look.isReducedMotion,\n\t\t\tpending,\n\t\t\trange: [this.min, Math.max(this.min, this.max)],\n\t\t\tsize,\n\t\t\tzoom: this.zoom,\n\t\t});\n\t\tconst { seconds, view } = timeline;\n\n\t\tthis.#drawn = { seconds, startSeconds: view.startSeconds };\n\t\tplaceLine(this.#playhead, timeline.playheadAt);\n\t\tplaceLine(this.#ghost, timeline.ghostAt);\n\t\tthis.showCurrentMarker(seconds);\n\t\tthis.toggleState('pending', view.pending.length > 0);\n\t\tthis.#rider.place({\n\t\t\tplayheadSeconds: seconds,\n\t\t\tstartSeconds: view.startSeconds,\n\t\t\twidthPx: view.width / view.dpr,\n\t\t\twindowSeconds: view.width / view.pixelsPerSecond,\n\t\t});\n\t\tif (isDirty || timeline.paintKey !== this.#paintedKey) {\n\t\t\tthis.#paintedKey = timeline.paintKey;\n\t\t\tthis.#paintScene(context, { ...view, colours: look.colours }, isRebuilt);\n\t\t\tif (timeline.wanted) this.#ask(timeline.wanted);\n\t\t}\n\t\tif (timeline.isMoving) this.surface()?.requestFrame();\n\t}\n\n\tprotected override renderMarkers(): void {\n\t\tconst bounds = this.mapping().bounds;\n\n\t\tthis.#rider.measure(\n\t\t\tthis.markers.flatMap(({ dimmed, kind, label, start }) =>\n\t\t\t\tlabel === undefined || label === ''\n\t\t\t\t\t? []\n\t\t\t\t\t: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tisDimmed: dimmed === true,\n\t\t\t\t\t\t\t\t...(kind === undefined ? {} : { kind }),\n\t\t\t\t\t\t\t\tstart: clamp(start, ...bounds),\n\t\t\t\t\t\t\t\ttext: label,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t),\n\t\t);\n\t\tthis.surface()?.rebuild();\n\t}\n\n\t#ask(wanted: [number, number]): void {\n\t\tconst asked = this.#requestPeaks?.(...wanted);\n\t\tif (!(asked instanceof Promise)) return;\n\n\t\tthis.#asked = asked;\n\t\tvoid this.#repaintAfter(asked);\n\t}\n\n\t#duePending(): Array<[number, number]> {\n\t\tconst due = dueRegions(this.#pending, this.#pendingListedMs, [\n\t\t\tperformance.now(),\n\t\t\tthis.pendingDelay,\n\t\t]);\n\n\t\tif (due.length < this.#pending.length) this.surface()?.requestFrame();\n\n\t\treturn due;\n\t}\n\n\t#grab(): ValueAxis {\n\t\tconst drawn = this.#drawn ?? { seconds: this.value, startSeconds: this.value };\n\n\t\tthis.#grabbed = drawn;\n\n\t\tconst mapping = this.mapping();\n\t\tconst [low, high] = mapping.bounds;\n\n\t\treturn {\n\t\t\tfromProportion: mapping.proportionOf(drawn.seconds),\n\t\t\tisKeptOnCancel: true,\n\t\t\tposition: (event) => -event.clientX,\n\t\t\ttravelPx: Math.max(1, (high - low) * this.zoom),\n\t\t};\n\t}\n\n\t#held(): undefined | { grabbed: Drawn; seconds: number } {\n\t\tconst grabbed = this.#grabbed;\n\t\tconst from = this.heldFrom();\n\t\tif (!grabbed || from === undefined) return undefined;\n\n\t\treturn { grabbed, seconds: this.value === from ? grabbed.seconds : this.value };\n\t}\n\n\t#paintMarkers(context: CanvasRenderingContext2D, scene: Scene, isRebuilt: boolean): void {\n\t\tif (isRebuilt) this.#kindColours = this.#readKindColours();\n\n\t\tconst line = Math.max(1, Math.round(scene.dpr));\n\n\t\tfor (const { dimmed, kind, start } of this.markers) {\n\t\t\tconst x = (start - scene.startSeconds) * scene.pixelsPerSecond;\n\n\t\t\tcontext.fillStyle =\n\t\t\t\t(isKind(kind) ? this.#kindColours.get(kind) : undefined) ?? scene.colours.marker;\n\t\t\tcontext.globalAlpha = dimmed === true ? 0.5 : 1;\n\t\t\tcontext.fillRect(x - line / 2, 0, line, scene.height);\n\t\t}\n\t\tcontext.globalAlpha = 1;\n\t}\n\n\t#paintScene(context: CanvasRenderingContext2D, scene: Scene, isRebuilt: boolean): void {\n\t\tconst { dpr, height, pixelsPerSecond, startSeconds, width } = scene;\n\t\tconst x = (seconds: number): number => (seconds - startSeconds) * pixelsPerSecond;\n\t\tconst { ends, grid } = scene.colours;\n\t\tconst line = Math.max(1, Math.round(dpr));\n\t\tconst [opening, closing] = [x(this.min), x(Math.max(this.min, this.max))];\n\t\tconst [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];\n\n\t\tcontext.clearRect(0, 0, width, height);\n\t\tpaintHatch(context, scene, [0, from]);\n\t\tpaintHatch(context, scene, [to, width]);\n\t\tcontext.fillStyle = grid;\n\t\tif (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);\n\t\tpaintPlaceholders(context, scene);\n\t\tif (this.#peaks) paintWave(context, scene, this.#peaks);\n\t\t// Scrolling lines sit at fractional x; rounding them judders\n\t\tcontext.fillStyle = ends;\n\t\tfor (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);\n\t\tthis.#paintMarkers(context, scene, isRebuilt);\n\t}\n\n\t#readKindColours(): Map<string, string> {\n\t\tconst kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];\n\t\tconst probes = kinds.map((kind) => {\n\t\t\tconst probe = document.createElement('div');\n\n\t\t\tprobe.className = 'sonic-waveform-label';\n\t\t\twriteKind(probe, kind, '--_sonic-waveform-marker');\n\n\t\t\treturn probe;\n\t\t});\n\n\t\tthis.control.append(...probes);\n\n\t\tconst read = probes.map((probe) => getComputedStyle(probe).color);\n\n\t\tfor (const probe of probes) probe.remove();\n\n\t\treturn new Map(kinds.map((kind, index) => [kind, read[index] ?? '']));\n\t}\n\n\tasync #repaintAfter(asked: Promise<unknown>): Promise<void> {\n\t\tawait Promise.allSettled([asked]);\n\t\tif (this.#asked !== asked) return;\n\n\t\tthis.#asked = undefined;\n\t\tthis.surface()?.invalidate();\n\t}\n\n\t#sourceSeconds(): number {\n\t\treturn this.#readTime?.() ?? this.playback();\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"waveform.js","names":[],"sources":["../../../src/elements/waveform.ts"],"sourcesContent":["import type { ValueAxis } from '#elements/value-gestures.ts';\nimport type { SurfaceFrame, SurfaceLook } from '#lib/canvas-surface.ts';\nimport type { Drawn, View } from '#lib/frame-timeline.ts';\nimport type { TimeRegions } from '#lib/time-regions.ts';\nimport type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nimport { SonicWaveElement } from '#elements/wave-element.ts';\nimport { frameTimeline } from '#lib/frame-timeline.ts';\nimport { isKind, writeKind } from '#lib/marker-band.ts';\nimport { createLabelRider } from '#lib/marker-rider.ts';\nimport { clamp } from '#lib/math.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { dueRegions, readRegions } from '#lib/time-regions.ts';\nimport { createTrackingClock } from '#lib/tracking-clock.ts';\nimport { waveformBuckets } from '#lib/waveform-buckets.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-waveform': SonicWaveform;\n\t}\n}\n\ntype Colour = keyof typeof colours;\n\nexport type PeaksRequest = (\n\tfromSeconds: number,\n\ttoSeconds: number,\n) => Iterable<Promise<unknown>> | Promise<unknown> | undefined;\n\ninterface Scene extends View {\n\tcolours: SurfaceLook<Colour>['colours'];\n}\n\nconst colours = {\n\tends: '--_sonic-waveform-ends',\n\tgrid: '--_sonic-waveform-grid',\n\tmarker: '--_sonic-waveform-marker',\n\tplaceholder: '--_sonic-waveform-placeholder',\n\twave: '--_sonic-waveform-wave',\n\twaveEdge: '--_sonic-waveform-wave-edge',\n} as const;\n\nconst defaultZoom = 70;\n\nconst amplitudeMargin = 0.94;\n\nconst hatchPitchPx = 8;\nconst placeholderWavelengthPx = 48;\nconst placeholderAmplitude = 0.2;\n\nconst renderWaveform = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-waveform\" role=\"slider\" tabindex=\"0\">\n\t\t\t<canvas class=\"sonic-waveform-canvas\" aria-hidden=\"true\"></canvas>\n\t\t\t<div class=\"sonic-waveform-scrim\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-waveform-ghost\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-playhead\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-waveform-label\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-label\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-waveform-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nfunction placeLine(line: HTMLElement, at: number | undefined): void {\n\tconst isHidden = at === undefined || at < 0 || at > 1;\n\n\tif (line.hidden !== isHidden) line.hidden = isHidden;\n\tif (isHidden) return;\n\n\tconst translate = `${String(at * 100)}cqi`;\n\n\tif (line.style.getPropertyValue('translate') !== translate) {\n\t\tline.style.setProperty('translate', translate);\n\t}\n}\n\nfunction paintHatch(\n\tcontext: CanvasRenderingContext2D,\n\tscene: Scene,\n\t[from, to]: [number, number],\n): void {\n\tif (to <= from) return;\n\n\tconst { dpr, height } = scene;\n\tconst pitch = Math.max(2, Math.round(hatchPitchPx * dpr));\n\tconst offset = scene.startSeconds * scene.pixelsPerSecond;\n\n\tcontext.save();\n\tcontext.beginPath();\n\tcontext.rect(from, 0, to - from, height);\n\tcontext.clip();\n\tcontext.beginPath();\n\tfor (let x = Math.floor((from - height + offset) / pitch) * pitch - offset; x < to; x += pitch) {\n\t\tcontext.moveTo(x, height);\n\t\tcontext.lineTo(x + height, 0);\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.ends;\n\tcontext.stroke();\n\tcontext.restore();\n}\n\nfunction paintPlaceholders(context: CanvasRenderingContext2D, scene: Scene): void {\n\tif (scene.pending.length === 0) return;\n\n\tconst { dpr, height, pixelsPerSecond, startSeconds } = scene;\n\tconst wavelength = placeholderWavelengthPx * dpr;\n\tconst center = height / 2;\n\tconst origin = startSeconds * pixelsPerSecond;\n\tconst waveY = (x: number): number =>\n\t\tcenter -\n\t\tMath.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) *\n\t\t\tcenter *\n\t\t\tplaceholderAmplitude;\n\n\tcontext.beginPath();\n\tfor (const [from, to] of scene.pending) {\n\t\tconst opening = (from - startSeconds) * pixelsPerSecond;\n\t\tconst closing = (to - startSeconds) * pixelsPerSecond;\n\n\t\tcontext.moveTo(opening, waveY(opening));\n\t\tfor (let x = opening + dpr; x < closing; x += dpr) context.lineTo(x, waveY(x));\n\t\tcontext.lineTo(closing, waveY(closing));\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.placeholder;\n\tcontext.stroke();\n}\n\nfunction waveFill(context: CanvasRenderingContext2D, scene: Scene): CanvasGradient {\n\tconst { wave, waveEdge } = scene.colours;\n\tconst gradient = context.createLinearGradient(0, 0, 0, scene.height);\n\n\tgradient.addColorStop(0, waveEdge);\n\tgradient.addColorStop(0.5, wave);\n\tgradient.addColorStop(1, waveEdge);\n\n\treturn gradient;\n}\n\nfunction paintWave(context: CanvasRenderingContext2D, scene: Scene, peaks: WaveformPeaks): void {\n\tconst buckets = waveformBuckets(peaks, scene);\n\tconst [first] = buckets;\n\tif (!first) return;\n\n\tconst center = scene.height / 2;\n\tconst amplitudePx = center * amplitudeMargin;\n\n\tcontext.fillStyle = waveFill(context, scene);\n\tcontext.beginPath();\n\tcontext.moveTo(first.x, center);\n\tfor (const bucket of buckets) context.lineTo(bucket.x, center - bucket.high * amplitudePx);\n\tfor (const bucket of buckets.toReversed())\n\t\tcontext.lineTo(bucket.x, center - bucket.low * amplitudePx);\n\tcontext.closePath();\n\tcontext.fill();\n}\n\nexport class SonicWaveform extends SonicWaveElement<Colour> {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicWaveElement.observedAttributes,\n\t\t'pending-delay',\n\t\t'playing',\n\t\t'playback-rate',\n\t\t'reduced-motion',\n\t\t'zoom',\n\t];\n\n\tget peaks(): undefined | WaveformPeaks {\n\t\treturn this.#peaks;\n\t}\n\n\tset peaks(peaks: undefined | WaveformPeaks) {\n\t\tthis.#peaks = peaks;\n\t\tthis.renderEmpty();\n\t\tthis.#repaint();\n\t}\n\n\tget pending(): Array<[number, number]> {\n\t\treturn this.#pending.map(([start, end]) => [start, end]);\n\t}\n\n\tset pending(regions: TimeRegions | undefined) {\n\t\tconst pending = regions ? readRegions(regions) : [];\n\t\tif (String(pending) === String(this.#pending)) return;\n\n\t\tconst listedMs = this.#pendingListedMs;\n\t\tconst nowMs = performance.now();\n\n\t\tthis.#pending = pending;\n\t\tthis.#pendingListedMs = new Map(\n\t\t\tthis.#pending.map((region) => {\n\t\t\t\tconst key = region.join(':');\n\n\t\t\t\treturn [key, listedMs.get(key) ?? nowMs];\n\t\t\t}),\n\t\t);\n\t\tthis.#repaint();\n\t}\n\n\tget pendingDelay(): number {\n\t\treturn Math.max(0, this.numberAttribute('pending-delay', 0));\n\t}\n\n\tset pendingDelay(value: number | undefined) {\n\t\tthis.reflect('pending-delay', value);\n\t}\n\n\tget playbackRate(): number {\n\t\tconst playbackRate = this.numberAttribute('playback-rate', 1);\n\n\t\treturn playbackRate > 0 ? playbackRate : 1;\n\t}\n\n\tset playbackRate(value: number | undefined) {\n\t\tthis.reflect('playback-rate', value);\n\t}\n\n\tget playing(): boolean {\n\t\treturn this.hasAttribute('playing');\n\t}\n\n\tset playing(isPlaying: boolean) {\n\t\tthis.reflect('playing', isPlaying);\n\t}\n\n\tget readTime(): (() => number | undefined) | undefined {\n\t\treturn this.#readTime;\n\t}\n\n\tset readTime(read: (() => number | undefined) | undefined) {\n\t\tthis.#readTime = read;\n\t\tthis.render();\n\t}\n\n\tget reducedMotion(): 'page' | 'scroll' {\n\t\treturn this.getAttribute('reduced-motion') === 'scroll' ? 'scroll' : 'page';\n\t}\n\n\tset reducedMotion(motion: 'page' | 'scroll' | undefined) {\n\t\tthis.reflect('reduced-motion', motion);\n\t}\n\n\tget requestPeaks(): PeaksRequest | undefined {\n\t\treturn this.#requestPeaks;\n\t}\n\n\tset requestPeaks(request: PeaksRequest | undefined) {\n\t\tthis.#requestPeaks = request;\n\t\tthis.#askedKey = undefined;\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tget zoom(): number {\n\t\tconst zoom = this.numberAttribute('zoom', defaultZoom);\n\n\t\treturn zoom > 0 ? zoom : defaultZoom;\n\t}\n\n\tset zoom(value: number | undefined) {\n\t\tthis.reflect('zoom', value);\n\t}\n\n\tprotected readonly control = renderWaveform();\n\n\tprotected readonly canvas = requireChild(\n\t\tthis.control,\n\t\t'.sonic-waveform-canvas',\n\t\tHTMLCanvasElement,\n\t);\n\n\tprotected readonly colours = colours;\n\n\tprotected readonly lengths = {};\n\n\tprotected readonly sheet = 'waveform.css';\n\n\tprotected readonly sizeProperty = '--_sonic-waveform-size';\n\n\treadonly #asked = new WeakSet<Promise<unknown>>();\n\n\t#askedKey: string | undefined;\n\n\treadonly #clock = createTrackingClock();\n\n\t#drawn: Drawn | undefined;\n\n\treadonly #ghost = requireChild(this.control, '.sonic-waveform-ghost', HTMLDivElement);\n\n\t#grabbed: Drawn | undefined;\n\n\t#isAsking = false;\n\n\t#kindColours = new Map<string, string>();\n\n\t#paintedKey = '';\n\n\t#peaks: undefined | WaveformPeaks;\n\n\t#pending: Array<[number, number]> = [];\n\n\t#pendingListedMs = new Map<string, number>();\n\n\treadonly #playhead = requireChild(this.control, '.sonic-waveform-playhead', HTMLDivElement);\n\n\t#readTime: (() => number | undefined) | undefined;\n\n\t#requestPeaks: PeaksRequest | undefined;\n\n\treadonly #rider = createLabelRider(this.control, {\n\t\tclassName: 'sonic-waveform-label',\n\t\tcolourProperty: '--_sonic-waveform-marker',\n\t\tfadeProperty: '--_sonic-waveform-label-fade',\n\t\tinsetProperty: '--_sonic-waveform-label-inset',\n\t\tparkProperty: '--_sonic-waveform-label-park',\n\t});\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties(\n\t\t\t'peaks',\n\t\t\t'pending',\n\t\t\t'pendingDelay',\n\t\t\t'playing',\n\t\t\t'playbackRate',\n\t\t\t'readTime',\n\t\t\t'reducedMotion',\n\t\t\t'requestPeaks',\n\t\t\t'zoom',\n\t\t);\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tsuper.connect(signal);\n\t\tthis.bindGestures(this.control, signal, () => this.#grab());\n\t\tif (!('fonts' in document)) return;\n\n\t\tdocument.fonts.addEventListener(\n\t\t\t'loadingdone',\n\t\t\t() => {\n\t\t\t\tthis.renderMarkers();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\tprotected draw(): void {\n\t\tconst surface = this.surface();\n\n\t\tsurface?.requestFrame();\n\t\t// No frame follows the position out of view, or before the first one paints\n\t\tif (surface?.isVisible === false || !this.#drawn) this.showCurrentMarker(this.#sourceSeconds());\n\t}\n\n\tprotected isEmpty(): boolean {\n\t\treturn !this.#peaks || this.#peaks.samples.length === 0;\n\t}\n\n\tprotected paint(\n\t\tcontext: CanvasRenderingContext2D,\n\t\t{ frameMs, isDirty, isRebuilt, look, size }: SurfaceFrame<Colour>,\n\t): void {\n\t\tconst held = this.#held();\n\t\tconst pending = this.#duePending();\n\t\tconst timeline = frameTimeline({\n\t\t\tclockSeconds: this.#clock.read(frameMs, {\n\t\t\t\tisPlaying: this.playing,\n\t\t\t\tplaybackRate: this.playbackRate,\n\t\t\t\tseconds: this.#sourceSeconds(),\n\t\t\t}),\n\t\t\tframeMs,\n\t\t\t...(held ? { held } : {}),\n\t\t\tisPaged: look.isReducedMotion && this.reducedMotion === 'page',\n\t\t\tisPlaying: this.playing,\n\t\t\tisStill: look.isReducedMotion,\n\t\t\tpending,\n\t\t\trange: [this.min, Math.max(this.min, this.max)],\n\t\t\tsize,\n\t\t\tzoom: this.zoom,\n\t\t});\n\t\tconst { seconds, view } = timeline;\n\n\t\tthis.#drawn = { seconds, startSeconds: view.startSeconds };\n\t\tplaceLine(this.#playhead, timeline.playheadAt);\n\t\tplaceLine(this.#ghost, timeline.ghostAt);\n\t\tthis.showCurrentMarker(seconds);\n\t\tthis.toggleState('pending', view.pending.length > 0);\n\t\tthis.#rider.place({\n\t\t\tplayheadSeconds: seconds,\n\t\t\tstartSeconds: view.startSeconds,\n\t\t\twidthPx: view.width / view.dpr,\n\t\t\twindowSeconds: view.width / view.pixelsPerSecond,\n\t\t});\n\t\tif (isDirty || timeline.paintKey !== this.#paintedKey) {\n\t\t\tthis.#paintedKey = timeline.paintKey;\n\t\t\tthis.#paintScene(context, { ...view, colours: look.colours }, isRebuilt);\n\t\t\tif (timeline.wanted) this.#ask(timeline.wanted);\n\t\t}\n\t\tif (timeline.isMoving) this.surface()?.requestFrame();\n\t}\n\n\tprotected override renderMarkers(): void {\n\t\tconst bounds = this.mapping().bounds;\n\n\t\tthis.#rider.measure(\n\t\t\tthis.markers.flatMap(({ dimmed, kind, label, start }) =>\n\t\t\t\tlabel === undefined || label === ''\n\t\t\t\t\t? []\n\t\t\t\t\t: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tisDimmed: dimmed === true,\n\t\t\t\t\t\t\t\t...(kind === undefined ? {} : { kind }),\n\t\t\t\t\t\t\t\tstart: clamp(start, ...bounds),\n\t\t\t\t\t\t\t\ttext: label,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t),\n\t\t);\n\t\tthis.surface()?.rebuild();\n\t}\n\n\t#ask(wanted: [number, number]): void {\n\t\tconst key = wanted.join(':');\n\t\tif (key === this.#askedKey) return;\n\n\t\tlet asked: ReturnType<PeaksRequest>;\n\n\t\tthis.#askedKey = key;\n\t\tthis.#isAsking = true;\n\t\ttry {\n\t\t\tasked = this.#requestPeaks?.(...wanted);\n\t\t} finally {\n\t\t\tthis.#isAsking = false;\n\t\t}\n\n\t\tconst promises = asked instanceof Promise ? [asked] : (asked ?? []);\n\n\t\tfor (const promise of promises) {\n\t\t\tif (this.#asked.has(promise)) continue;\n\n\t\t\tthis.#asked.add(promise);\n\t\t\tvoid this.#repaintAfter(promise);\n\t\t}\n\t}\n\n\t#duePending(): Array<[number, number]> {\n\t\tconst due = dueRegions(this.#pending, this.#pendingListedMs, [\n\t\t\tperformance.now(),\n\t\t\tthis.pendingDelay,\n\t\t]);\n\n\t\tif (due.length < this.#pending.length) this.surface()?.requestFrame();\n\n\t\treturn due;\n\t}\n\n\t#grab(): ValueAxis {\n\t\tconst drawn = this.#drawn ?? { seconds: this.value, startSeconds: this.value };\n\n\t\tthis.#grabbed = drawn;\n\n\t\tconst mapping = this.mapping();\n\t\tconst [low, high] = mapping.bounds;\n\n\t\treturn {\n\t\t\tfromProportion: mapping.proportionOf(drawn.seconds),\n\t\t\tisKeptOnCancel: true,\n\t\t\tposition: (event) => -event.clientX,\n\t\t\ttravelPx: Math.max(1, (high - low) * this.zoom),\n\t\t};\n\t}\n\n\t#held(): undefined | { grabbed: Drawn; seconds: number } {\n\t\tconst grabbed = this.#grabbed;\n\t\tconst from = this.heldFrom();\n\t\tif (!grabbed || from === undefined) return undefined;\n\n\t\treturn { grabbed, seconds: this.value === from ? grabbed.seconds : this.value };\n\t}\n\n\t#paintMarkers(context: CanvasRenderingContext2D, scene: Scene, isRebuilt: boolean): void {\n\t\tif (isRebuilt) this.#kindColours = this.#readKindColours();\n\n\t\tconst line = Math.max(1, Math.round(scene.dpr));\n\n\t\tfor (const { dimmed, kind, start } of this.markers) {\n\t\t\tconst x = (start - scene.startSeconds) * scene.pixelsPerSecond;\n\n\t\t\tcontext.fillStyle =\n\t\t\t\t(isKind(kind) ? this.#kindColours.get(kind) : undefined) ?? scene.colours.marker;\n\t\t\tcontext.globalAlpha = dimmed === true ? 0.5 : 1;\n\t\t\tcontext.fillRect(x - line / 2, 0, line, scene.height);\n\t\t}\n\t\tcontext.globalAlpha = 1;\n\t}\n\n\t#paintScene(context: CanvasRenderingContext2D, scene: Scene, isRebuilt: boolean): void {\n\t\tconst { dpr, height, pixelsPerSecond, startSeconds, width } = scene;\n\t\tconst x = (seconds: number): number => (seconds - startSeconds) * pixelsPerSecond;\n\t\tconst { ends, grid } = scene.colours;\n\t\tconst line = Math.max(1, Math.round(dpr));\n\t\tconst [opening, closing] = [x(this.min), x(Math.max(this.min, this.max))];\n\t\tconst [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];\n\n\t\tcontext.clearRect(0, 0, width, height);\n\t\tpaintHatch(context, scene, [0, from]);\n\t\tpaintHatch(context, scene, [to, width]);\n\t\tcontext.fillStyle = grid;\n\t\tif (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);\n\t\tpaintPlaceholders(context, scene);\n\t\tif (this.#peaks) paintWave(context, scene, this.#peaks);\n\t\t// Scrolling lines sit at fractional x; rounding them judders\n\t\tcontext.fillStyle = ends;\n\t\tfor (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);\n\t\tthis.#paintMarkers(context, scene, isRebuilt);\n\t}\n\n\t#readKindColours(): Map<string, string> {\n\t\tconst kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];\n\t\tconst probes = kinds.map((kind) => {\n\t\t\tconst probe = document.createElement('div');\n\n\t\t\tprobe.className = 'sonic-waveform-label';\n\t\t\twriteKind(probe, kind, '--_sonic-waveform-marker');\n\n\t\t\treturn probe;\n\t\t});\n\n\t\tthis.control.append(...probes);\n\n\t\tconst read = probes.map((probe) => getComputedStyle(probe).color);\n\n\t\tfor (const probe of probes) probe.remove();\n\n\t\treturn new Map(kinds.map((kind, index) => [kind, read[index] ?? '']));\n\t}\n\n\t// A write from inside the consumer's call repaints without asking for the same window again\n\t#repaint(): void {\n\t\tif (!this.#isAsking) this.#askedKey = undefined;\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tasync #repaintAfter(asked: Promise<unknown>): Promise<void> {\n\t\tawait Promise.allSettled([asked]);\n\t\tthis.#repaint();\n\t}\n\n\t#sourceSeconds(): number {\n\t\treturn this.#readTime?.() ?? this.playback();\n\t}\n}\n"],"mappings":"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"}
@@ -18,6 +18,7 @@ declare const colours: {
18
18
  declare const lengths: {
19
19
  readonly gapRatio: "--_sonic-wavestrip-bar-gap-ratio";
20
20
  readonly grooveRatio: "--_sonic-wavestrip-groove-ratio";
21
+ readonly grooveSize: "--_sonic-wavestrip-groove-size";
21
22
  readonly pitch: "--_sonic-wavestrip-bar-pitch";
22
23
  readonly radiusRatio: "--_sonic-wavestrip-bar-radius-ratio";
23
24
  };
@@ -40,11 +41,13 @@ export declare class SonicWavestrip extends SonicWaveElement<Colour, Length> {
40
41
  protected readonly lengths: {
41
42
  readonly gapRatio: "--_sonic-wavestrip-bar-gap-ratio";
42
43
  readonly grooveRatio: "--_sonic-wavestrip-groove-ratio";
44
+ readonly grooveSize: "--_sonic-wavestrip-groove-size";
43
45
  readonly pitch: "--_sonic-wavestrip-bar-pitch";
44
46
  readonly radiusRatio: "--_sonic-wavestrip-bar-radius-ratio";
45
47
  };
46
48
  protected readonly sheet = "wavestrip.css";
47
49
  protected readonly sizeProperty = "--_sonic-wavestrip-size";
50
+ clientXOf(value: number): number;
48
51
  connectedCallback(): void;
49
52
  protected connect(signal: AbortSignal): void;
50
53
  protected draw(): void;
@@ -16,6 +16,7 @@ const colours = {
16
16
  const lengths = {
17
17
  gapRatio: "--_sonic-wavestrip-bar-gap-ratio",
18
18
  grooveRatio: "--_sonic-wavestrip-groove-ratio",
19
+ grooveSize: "--_sonic-wavestrip-groove-size",
19
20
  pitch: "--_sonic-wavestrip-bar-pitch",
20
21
  radiusRatio: "--_sonic-wavestrip-bar-radius-ratio"
21
22
  };
@@ -79,6 +80,10 @@ var SonicWavestrip = class extends SonicWaveElement {
79
80
  #markerBand = createMarkerBand(requireChild(this.control, ".sonic-wavestrip-markers", HTMLDivElement));
80
81
  #paintedKey = "";
81
82
  #peaks;
83
+ clientXOf(value) {
84
+ const { startPx, travelPx } = this.#axis();
85
+ return startPx + this.mapping().proportionOf(value) * travelPx;
86
+ }
82
87
  connectedCallback() {
83
88
  this.upgradeProperties("buffered", "cancellable", "peaks");
84
89
  super.connectedCallback();
@@ -141,7 +146,7 @@ var SonicWavestrip = class extends SonicWaveElement {
141
146
  #barsFor({ isDirty, look, size }) {
142
147
  if (!isDirty && this.#bars) return this.#bars;
143
148
  const peaks = this.#peaks;
144
- const groove = stripGroove(size, look.lengths.grooveRatio);
149
+ const groove = stripGroove(size, look.lengths);
145
150
  const drawn = groove ? [groove] : [];
146
151
  const bars = peaks && peaks.length > 0 ? stripBars(peaks, size, look.lengths) : drawn;
147
152
  this.#bars = void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"wavestrip.js","names":[],"sources":["../../../src/elements/wavestrip.ts"],"sourcesContent":["import type { ValueAxis } from '#elements/value-gestures.ts';\nimport type { SurfaceFrame } from '#lib/canvas-surface.ts';\nimport type { StripRegions } from '#lib/strip-scene.ts';\nimport type { TimeRegions } from '#lib/time-regions.ts';\n\nimport { SonicWaveElement } from '#elements/wave-element.ts';\nimport { createMarkerBand } from '#lib/marker-band.ts';\nimport { clampProportion } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { stripBars, stripGroove, stripRegions } from '#lib/strip-scene.ts';\nimport { sortedRegions } from '#lib/time-regions.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-wavestrip': SonicWavestrip;\n\t}\n}\n\ntype Colour = keyof typeof colours;\n\ntype Length = keyof typeof lengths;\n\nconst colours = {\n\tbuffered: '--_sonic-wavestrip-buffered',\n\tplayed: '--_sonic-wavestrip-played',\n\tscrub: '--_sonic-wavestrip-scrub',\n\twave: '--_sonic-wavestrip-wave',\n} as const;\n\nconst lengths = {\n\tgapRatio: '--_sonic-wavestrip-bar-gap-ratio',\n\tgrooveRatio: '--_sonic-wavestrip-groove-ratio',\n\tpitch: '--_sonic-wavestrip-bar-pitch',\n\tradiusRatio: '--_sonic-wavestrip-bar-radius-ratio',\n} as const;\n\nconst cancelZonePx = 48;\n\nconst renderWavestrip = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-wavestrip\" role=\"slider\" tabindex=\"0\">\n\t\t\t<canvas class=\"sonic-wavestrip-canvas\" aria-hidden=\"true\"></canvas>\n\t\t\t<div class=\"sonic-wavestrip-markers\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-wavestrip-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-wavestrip-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nfunction fillRegion(\n\tcontext: CanvasRenderingContext2D,\n\tpath: Path2D,\n\t[from, to, style]: [number, number, string],\n): void {\n\tcontext.save();\n\tcontext.beginPath();\n\tcontext.rect(from, 0, to - from, context.canvas.height);\n\tcontext.clip();\n\tcontext.fillStyle = style;\n\tcontext.fill(path);\n\tcontext.restore();\n}\n\nexport class SonicWavestrip extends SonicWaveElement<Colour, Length> {\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per property\n\tget buffered(): Array<[number, number]> {\n\t\treturn this.#buffered.map(([start, end]) => [start, end]);\n\t}\n\n\tset buffered(regions: TimeRegions | undefined) {\n\t\tthis.#buffered = sortedRegions(regions);\n\t\tthis.render();\n\t}\n\n\tget cancellable(): boolean {\n\t\treturn this.hasAttribute('cancellable');\n\t}\n\n\tset cancellable(isCancellable: boolean) {\n\t\tthis.reflect('cancellable', isCancellable);\n\t}\n\n\tget peaks(): ArrayLike<number> | undefined {\n\t\treturn this.#peaks;\n\t}\n\n\tset peaks(peaks: ArrayLike<number> | undefined) {\n\t\tthis.#peaks = peaks;\n\t\tthis.renderEmpty();\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tprotected readonly control = renderWavestrip();\n\n\tprotected readonly canvas = requireChild(\n\t\tthis.control,\n\t\t'.sonic-wavestrip-canvas',\n\t\tHTMLCanvasElement,\n\t);\n\n\tprotected readonly colours = colours;\n\n\tprotected readonly lengths = lengths;\n\n\tprotected readonly sheet = 'wavestrip.css';\n\n\tprotected readonly sizeProperty = '--_sonic-wavestrip-size';\n\n\t#bars: Path2D | undefined;\n\n\t#buffered: Array<[number, number]> = [];\n\n\treadonly #markerBand = createMarkerBand(\n\t\trequireChild(this.control, '.sonic-wavestrip-markers', HTMLDivElement),\n\t);\n\n\t#paintedKey = '';\n\n\t#peaks: ArrayLike<number> | undefined;\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('buffered', 'cancellable', 'peaks');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tconst { control } = this;\n\n\t\tsuper.connect(signal);\n\t\tthis.bindHover(control, signal, {\n\t\t\tplace: () => {\n\t\t\t\tthis.#placeReadout();\n\t\t\t},\n\t\t\tvalueAt: (event) => {\n\t\t\t\tconst axis = this.#axis();\n\t\t\t\tconst at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);\n\t\t\t\tconst mapping = this.mapping();\n\n\t\t\t\treturn this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at));\n\t\t\t},\n\t\t});\n\t\tthis.bindGestures(control, signal, (event) => this.#grab(event));\n\t}\n\n\tprotected draw(): void {\n\t\tconst surface = this.surface();\n\n\t\tthis.#placeReadout();\n\t\tthis.showCurrentMarker(this.playback());\n\t\tif (surface && this.#stripRegions(surface.size.width).key !== this.#paintedKey) {\n\t\t\tsurface.requestFrame();\n\t\t}\n\t}\n\n\tprotected isEmpty(): boolean {\n\t\treturn !this.#peaks || this.#peaks.length === 0;\n\t}\n\n\tprotected paint(context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length>): void {\n\t\tconst { look, size } = frame;\n\t\tconst strip = this.#stripRegions(size.width);\n\t\tconst bars = this.#barsFor(frame);\n\n\t\tthis.#paintedKey = strip.key;\n\t\tcontext.clearRect(0, 0, size.width, size.height);\n\t\tif (!bars) return;\n\n\t\tcontext.fillStyle = look.colours.wave;\n\t\tcontext.fill(bars);\n\t\tfor (const { from, kind, to } of strip.regions) {\n\t\t\tfillRegion(context, bars, [from, to, look.colours[kind]]);\n\t\t}\n\t}\n\n\tprotected override renderMarkers(): void {\n\t\tthis.#markerBand.render(this.markerList(), this.markerProportions());\n\t}\n\n\tprotected override scrubsKeyRepeat(): boolean {\n\t\treturn true;\n\t}\n\n\t#axis(): ValueAxis & { startPx: number; topPx: number } {\n\t\tconst box = this.canvas.getBoundingClientRect();\n\n\t\treturn {\n\t\t\tposition: (event: PointerEvent) => event.clientX,\n\t\t\tstartPx: box.left,\n\t\t\ttopPx: box.top,\n\t\t\ttravelPx: Math.max(1, box.width),\n\t\t};\n\t}\n\n\t#barsFor({ isDirty, look, size }: SurfaceFrame<Colour, Length>): Path2D | undefined {\n\t\tif (!isDirty && this.#bars) return this.#bars;\n\n\t\tconst peaks = this.#peaks;\n\t\tconst groove = stripGroove(size, look.lengths.grooveRatio);\n\t\tconst drawn = groove ? [groove] : [];\n\t\tconst bars = peaks && peaks.length > 0 ? stripBars(peaks, size, look.lengths) : drawn;\n\n\t\tthis.#bars = undefined;\n\t\tif (bars.length === 0) return undefined;\n\n\t\tconst path = new Path2D();\n\n\t\tfor (const { height, radius, width, x, y } of bars) {\n\t\t\tpath.roundRect(x, y, width, height, radius);\n\t\t}\n\t\tthis.#bars = path;\n\n\t\treturn path;\n\t}\n\n\t#grab(event: PointerEvent): ValueAxis {\n\t\tconst axis = this.#axis();\n\t\tconst at = (event.clientX - axis.startPx) / axis.travelPx;\n\t\tconst outside = this.#outside();\n\t\tconst grabbed = {\n\t\t\tfromProportion: clampProportion(at),\n\t\t\tposition: axis.position,\n\t\t\ttravelPx: axis.travelPx,\n\t\t};\n\n\t\tthis.input(this.#markerAt(event, axis) ?? this.mapping().valueAt(at));\n\n\t\treturn outside ? { ...grabbed, outside } : grabbed;\n\t}\n\n\t#markerAt(\n\t\tevent: PointerEvent,\n\t\t{ startPx, topPx, travelPx }: { startPx: number; topPx: number; travelPx: number },\n\t): number | undefined {\n\t\tconst index = this.#markerBand.at(\n\t\t\t{ x: event.clientX - startPx, y: event.clientY - topPx },\n\t\t\ttravelPx,\n\t\t);\n\n\t\treturn index === undefined ? undefined : this.markerList()[index]?.start;\n\t}\n\n\t#outside(): ((event: PointerEvent) => boolean) | undefined {\n\t\tif (!this.cancellable) return undefined;\n\n\t\tconst box = this.control.getBoundingClientRect();\n\t\tconst zone = readPxProperty(\n\t\t\tgetComputedStyle(this.control),\n\t\t\t'--_sonic-cancel-zone',\n\t\t\tcancelZonePx,\n\t\t);\n\n\t\treturn (event) => event.clientY < box.top - zone || event.clientY > box.bottom + zone;\n\t}\n\n\t#placeReadout(): void {\n\t\tconst at = this.mapping().proportionOf(this.readoutValue());\n\n\t\tthis.control.style.setProperty('--_sonic-wavestrip-readout-at', String(at));\n\t}\n\n\t#stripRegions(width: number): StripRegions {\n\t\tconst { proportionOf } = this.mapping();\n\t\tconst isScrubbing = this.heldFrom() !== undefined && this.isRevealed();\n\n\t\treturn stripRegions(\n\t\t\t{\n\t\t\t\tbuffered: this.#buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),\n\t\t\t\tplayed: proportionOf(this.playback()),\n\t\t\t\t...(isScrubbing ? { scrub: proportionOf(this.value) } : {}),\n\t\t\t},\n\t\t\twidth,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"wavestrip.js","names":[],"sources":["../../../src/elements/wavestrip.ts"],"sourcesContent":["import type { ValueAxis } from '#elements/value-gestures.ts';\nimport type { SurfaceFrame } from '#lib/canvas-surface.ts';\nimport type { StripRegions } from '#lib/strip-scene.ts';\nimport type { TimeRegions } from '#lib/time-regions.ts';\n\nimport { SonicWaveElement } from '#elements/wave-element.ts';\nimport { createMarkerBand } from '#lib/marker-band.ts';\nimport { clampProportion } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { stripBars, stripGroove, stripRegions } from '#lib/strip-scene.ts';\nimport { sortedRegions } from '#lib/time-regions.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-wavestrip': SonicWavestrip;\n\t}\n}\n\ntype Colour = keyof typeof colours;\n\ntype Length = keyof typeof lengths;\n\nconst colours = {\n\tbuffered: '--_sonic-wavestrip-buffered',\n\tplayed: '--_sonic-wavestrip-played',\n\tscrub: '--_sonic-wavestrip-scrub',\n\twave: '--_sonic-wavestrip-wave',\n} as const;\n\nconst lengths = {\n\tgapRatio: '--_sonic-wavestrip-bar-gap-ratio',\n\tgrooveRatio: '--_sonic-wavestrip-groove-ratio',\n\tgrooveSize: '--_sonic-wavestrip-groove-size',\n\tpitch: '--_sonic-wavestrip-bar-pitch',\n\tradiusRatio: '--_sonic-wavestrip-bar-radius-ratio',\n} as const;\n\nconst cancelZonePx = 48;\n\nconst renderWavestrip = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-wavestrip\" role=\"slider\" tabindex=\"0\">\n\t\t\t<canvas class=\"sonic-wavestrip-canvas\" aria-hidden=\"true\"></canvas>\n\t\t\t<div class=\"sonic-wavestrip-markers\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-wavestrip-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-wavestrip-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nfunction fillRegion(\n\tcontext: CanvasRenderingContext2D,\n\tpath: Path2D,\n\t[from, to, style]: [number, number, string],\n): void {\n\tcontext.save();\n\tcontext.beginPath();\n\tcontext.rect(from, 0, to - from, context.canvas.height);\n\tcontext.clip();\n\tcontext.fillStyle = style;\n\tcontext.fill(path);\n\tcontext.restore();\n}\n\nexport class SonicWavestrip extends SonicWaveElement<Colour, Length> {\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per property\n\tget buffered(): Array<[number, number]> {\n\t\treturn this.#buffered.map(([start, end]) => [start, end]);\n\t}\n\n\tset buffered(regions: TimeRegions | undefined) {\n\t\tthis.#buffered = sortedRegions(regions);\n\t\tthis.render();\n\t}\n\n\tget cancellable(): boolean {\n\t\treturn this.hasAttribute('cancellable');\n\t}\n\n\tset cancellable(isCancellable: boolean) {\n\t\tthis.reflect('cancellable', isCancellable);\n\t}\n\n\tget peaks(): ArrayLike<number> | undefined {\n\t\treturn this.#peaks;\n\t}\n\n\tset peaks(peaks: ArrayLike<number> | undefined) {\n\t\tthis.#peaks = peaks;\n\t\tthis.renderEmpty();\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tprotected readonly control = renderWavestrip();\n\n\tprotected readonly canvas = requireChild(\n\t\tthis.control,\n\t\t'.sonic-wavestrip-canvas',\n\t\tHTMLCanvasElement,\n\t);\n\n\tprotected readonly colours = colours;\n\n\tprotected readonly lengths = lengths;\n\n\tprotected readonly sheet = 'wavestrip.css';\n\n\tprotected readonly sizeProperty = '--_sonic-wavestrip-size';\n\n\t#bars: Path2D | undefined;\n\n\t#buffered: Array<[number, number]> = [];\n\n\treadonly #markerBand = createMarkerBand(\n\t\trequireChild(this.control, '.sonic-wavestrip-markers', HTMLDivElement),\n\t);\n\n\t#paintedKey = '';\n\n\t#peaks: ArrayLike<number> | undefined;\n\n\tclientXOf(value: number): number {\n\t\tconst { startPx, travelPx } = this.#axis();\n\n\t\treturn startPx + this.mapping().proportionOf(value) * travelPx;\n\t}\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('buffered', 'cancellable', 'peaks');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tconst { control } = this;\n\n\t\tsuper.connect(signal);\n\t\tthis.bindHover(control, signal, {\n\t\t\tplace: () => {\n\t\t\t\tthis.#placeReadout();\n\t\t\t},\n\t\t\tvalueAt: (event) => {\n\t\t\t\tconst axis = this.#axis();\n\t\t\t\tconst at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);\n\t\t\t\tconst mapping = this.mapping();\n\n\t\t\t\treturn this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at));\n\t\t\t},\n\t\t});\n\t\tthis.bindGestures(control, signal, (event) => this.#grab(event));\n\t}\n\n\tprotected draw(): void {\n\t\tconst surface = this.surface();\n\n\t\tthis.#placeReadout();\n\t\tthis.showCurrentMarker(this.playback());\n\t\tif (surface && this.#stripRegions(surface.size.width).key !== this.#paintedKey) {\n\t\t\tsurface.requestFrame();\n\t\t}\n\t}\n\n\tprotected isEmpty(): boolean {\n\t\treturn !this.#peaks || this.#peaks.length === 0;\n\t}\n\n\tprotected paint(context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length>): void {\n\t\tconst { look, size } = frame;\n\t\tconst strip = this.#stripRegions(size.width);\n\t\tconst bars = this.#barsFor(frame);\n\n\t\tthis.#paintedKey = strip.key;\n\t\tcontext.clearRect(0, 0, size.width, size.height);\n\t\tif (!bars) return;\n\n\t\tcontext.fillStyle = look.colours.wave;\n\t\tcontext.fill(bars);\n\t\tfor (const { from, kind, to } of strip.regions) {\n\t\t\tfillRegion(context, bars, [from, to, look.colours[kind]]);\n\t\t}\n\t}\n\n\tprotected override renderMarkers(): void {\n\t\tthis.#markerBand.render(this.markerList(), this.markerProportions());\n\t}\n\n\tprotected override scrubsKeyRepeat(): boolean {\n\t\treturn true;\n\t}\n\n\t#axis(): ValueAxis & { startPx: number; topPx: number } {\n\t\tconst box = this.canvas.getBoundingClientRect();\n\n\t\treturn {\n\t\t\tposition: (event: PointerEvent) => event.clientX,\n\t\t\tstartPx: box.left,\n\t\t\ttopPx: box.top,\n\t\t\ttravelPx: Math.max(1, box.width),\n\t\t};\n\t}\n\n\t#barsFor({ isDirty, look, size }: SurfaceFrame<Colour, Length>): Path2D | undefined {\n\t\tif (!isDirty && this.#bars) return this.#bars;\n\n\t\tconst peaks = this.#peaks;\n\t\tconst groove = stripGroove(size, look.lengths);\n\t\tconst drawn = groove ? [groove] : [];\n\t\tconst bars = peaks && peaks.length > 0 ? stripBars(peaks, size, look.lengths) : drawn;\n\n\t\tthis.#bars = undefined;\n\t\tif (bars.length === 0) return undefined;\n\n\t\tconst path = new Path2D();\n\n\t\tfor (const { height, radius, width, x, y } of bars) {\n\t\t\tpath.roundRect(x, y, width, height, radius);\n\t\t}\n\t\tthis.#bars = path;\n\n\t\treturn path;\n\t}\n\n\t#grab(event: PointerEvent): ValueAxis {\n\t\tconst axis = this.#axis();\n\t\tconst at = (event.clientX - axis.startPx) / axis.travelPx;\n\t\tconst outside = this.#outside();\n\t\tconst grabbed = {\n\t\t\tfromProportion: clampProportion(at),\n\t\t\tposition: axis.position,\n\t\t\ttravelPx: axis.travelPx,\n\t\t};\n\n\t\tthis.input(this.#markerAt(event, axis) ?? this.mapping().valueAt(at));\n\n\t\treturn outside ? { ...grabbed, outside } : grabbed;\n\t}\n\n\t#markerAt(\n\t\tevent: PointerEvent,\n\t\t{ startPx, topPx, travelPx }: { startPx: number; topPx: number; travelPx: number },\n\t): number | undefined {\n\t\tconst index = this.#markerBand.at(\n\t\t\t{ x: event.clientX - startPx, y: event.clientY - topPx },\n\t\t\ttravelPx,\n\t\t);\n\n\t\treturn index === undefined ? undefined : this.markerList()[index]?.start;\n\t}\n\n\t#outside(): ((event: PointerEvent) => boolean) | undefined {\n\t\tif (!this.cancellable) return undefined;\n\n\t\tconst box = this.control.getBoundingClientRect();\n\t\tconst zone = readPxProperty(\n\t\t\tgetComputedStyle(this.control),\n\t\t\t'--_sonic-cancel-zone',\n\t\t\tcancelZonePx,\n\t\t);\n\n\t\treturn (event) => event.clientY < box.top - zone || event.clientY > box.bottom + zone;\n\t}\n\n\t#placeReadout(): void {\n\t\tconst at = this.mapping().proportionOf(this.readoutValue());\n\n\t\tthis.control.style.setProperty('--_sonic-wavestrip-readout-at', String(at));\n\t}\n\n\t#stripRegions(width: number): StripRegions {\n\t\tconst { proportionOf } = this.mapping();\n\t\tconst isScrubbing = this.heldFrom() !== undefined && this.isRevealed();\n\n\t\treturn stripRegions(\n\t\t\t{\n\t\t\t\tbuffered: this.#buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),\n\t\t\t\tplayed: proportionOf(this.playback()),\n\t\t\t\t...(isScrubbing ? { scrub: proportionOf(this.value) } : {}),\n\t\t\t},\n\t\t\twidth,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
@@ -10,11 +10,12 @@ import { SonicSlider } from "./elements/slider.js";
10
10
  import { SonicSpectrum } from "./elements/spectrum.js";
11
11
  import { SonicSplit } from "./elements/split.js";
12
12
  import { SonicSwitch } from "./elements/switch.js";
13
+ import { SonicToggle } from "./elements/toggle.js";
13
14
  import { WaveMarker } from "./elements/wave-element.js";
14
- import { SonicWaveform } from "./elements/waveform.js";
15
+ import { PeaksRequest, SonicWaveform } from "./elements/waveform.js";
15
16
  import { SonicWavestrip } from "./elements/wavestrip.js";
16
17
  import { SonicXy } from "./elements/xy.js";
17
18
  import { crossfadeGains } from "./lib/crossfade.js";
18
19
  import { envelopeCurve } from "./lib/envelope-curve.js";
19
20
  import { formatPercent, parsePercent } from "./lib/percent.js";
20
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, type TimeRegions, type WaveMarker, type WaveformPeaks, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
21
+ export { type PeaksRequest, SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, type TimeRegions, type WaveMarker, type WaveformPeaks, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
package/dist/dev/index.js CHANGED
@@ -9,10 +9,11 @@ import { SonicSlider } from "./elements/slider.js";
9
9
  import { SonicSpectrum } from "./elements/spectrum.js";
10
10
  import { SonicSplit } from "./elements/split.js";
11
11
  import { SonicSwitch } from "./elements/switch.js";
12
+ import { SonicToggle } from "./elements/toggle.js";
12
13
  import { SonicWaveform } from "./elements/waveform.js";
13
14
  import { SonicWavestrip } from "./elements/wavestrip.js";
14
15
  import { SonicXy } from "./elements/xy.js";
15
16
  import { crossfadeGains } from "./lib/crossfade.js";
16
17
  import { formatPercent, parsePercent } from "./lib/percent.js";
17
18
 
18
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
19
+ export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
@@ -0,0 +1,28 @@
1
+ //#region src/lib/key-press.ts
2
+ function bindKeyPress(root, signal) {
3
+ let pressed;
4
+ const release = () => {
5
+ if (!pressed) return;
6
+ delete pressed.dataset.sonicPressed;
7
+ pressed = void 0;
8
+ };
9
+ root.addEventListener("keydown", (event) => {
10
+ if (event.key !== " " || event.repeat || event.defaultPrevented) return;
11
+ const part = event.target;
12
+ if (!(part instanceof HTMLButtonElement) || part.disabled) return;
13
+ release();
14
+ pressed = part;
15
+ part.dataset.sonicPressed = "";
16
+ }, { signal });
17
+ root.addEventListener("keyup", (event) => {
18
+ if (event.key === " " || event.key === "Meta") release();
19
+ }, { signal });
20
+ root.addEventListener("focusout", (event) => {
21
+ if (event.target === pressed) release();
22
+ }, { signal });
23
+ signal.addEventListener("abort", release, { once: true });
24
+ }
25
+
26
+ //#endregion
27
+ export { bindKeyPress };
28
+ //# sourceMappingURL=key-press.js.map
@@ -0,0 +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"}
@@ -18,9 +18,10 @@ function stripBars(peaks, size, grid) {
18
18
  };
19
19
  });
20
20
  }
21
- function stripGroove(size, ratio) {
22
- if (ratio <= 0) return void 0;
23
- const height = Math.max(1, Math.round(size.height * ratio));
21
+ function stripGroove(size, { grooveRatio, grooveSize }) {
22
+ const thickness = grooveSize > 0 ? grooveSize * size.dpr : size.height * grooveRatio;
23
+ if (thickness <= 0) return void 0;
24
+ const height = Math.max(1, Math.round(Math.min(thickness, size.height)));
24
25
  return {
25
26
  height,
26
27
  radius: height / 2,
@@ -1 +1 @@
1
- {"version":3,"file":"strip-scene.js","names":[],"sources":["../../../src/lib/strip-scene.ts"],"sourcesContent":["import type { SurfaceSize } from '#lib/canvas-surface.ts';\n\nimport { clampProportion } from '#lib/math.ts';\nimport { resamplePeaks } from '#lib/resample-peaks.ts';\n\ninterface BarRect {\n\theight: number;\n\tradius: number;\n\twidth: number;\n\tx: number;\n\ty: number;\n}\n\ninterface BarGrid {\n\tgapRatio: number;\n\tpitch: number;\n\tradiusRatio: number;\n}\n\ninterface RegionState {\n\tbuffered: ReadonlyArray<[number, number]>;\n\tplayed: number;\n\tscrub?: number;\n}\n\nexport interface StripRegions {\n\tkey: string;\n\tregions: Array<{ from: number; kind: 'buffered' | 'played' | 'scrub'; to: number }>;\n}\n\n// Whole device pixels, or bars alias unevenly and Skia's CPU raster crawls\nexport function stripBars(\n\tpeaks: ArrayLike<number>,\n\tsize: SurfaceSize,\n\tgrid: BarGrid,\n): Array<BarRect> {\n\tconst pitch = Math.max(1, Math.round(grid.pitch * size.dpr));\n\tconst gap = Math.min(pitch - 1, Math.max(0, Math.round(pitch * grid.gapRatio)));\n\tconst width = pitch - gap;\n\tconst count = Math.max(0, Math.floor((size.width + gap) / pitch));\n\n\treturn resamplePeaks(peaks, count).map((peak, index) => {\n\t\tconst height = Math.max(1, Math.round(clampProportion(peak) * size.height));\n\n\t\treturn {\n\t\t\theight,\n\t\t\tradius: width * grid.radiusRatio,\n\t\t\twidth,\n\t\t\tx: index * pitch,\n\t\t\ty: Math.round((size.height - height) / 2),\n\t\t};\n\t});\n}\n\nexport function stripGroove(size: SurfaceSize, ratio: number): BarRect | undefined {\n\tif (ratio <= 0) return undefined;\n\n\tconst height = Math.max(1, Math.round(size.height * ratio));\n\n\treturn {\n\t\theight,\n\t\tradius: height / 2,\n\t\twidth: size.width,\n\t\tx: 0,\n\t\ty: Math.round((size.height - height) / 2),\n\t};\n}\n\nexport function stripRegions(state: RegionState, widthPx: number): StripRegions {\n\tconst px = (proportion: number): number =>\n\t\tMath.round((Number.isFinite(proportion) ? proportion : 0) * widthPx);\n\tconst buffered = state.buffered\n\t\t.map(([start, end]): [number, number] => [px(start), px(end)])\n\t\t.filter(([start, end]) => end > start);\n\tconst played = px(state.played);\n\tconst scrub = state.scrub === undefined ? played : px(state.scrub);\n\tconst regions: StripRegions['regions'] = buffered.map(([from, to]) => ({\n\t\tfrom,\n\t\tkind: 'buffered',\n\t\tto,\n\t}));\n\n\tif (played > 0) regions.push({ from: 0, kind: 'played', to: played });\n\tif (scrub !== played) {\n\t\tregions.push({ from: Math.min(played, scrub), kind: 'scrub', to: Math.max(played, scrub) });\n\t}\n\n\treturn { key: `${String(played)}:${String(scrub)}:${buffered.join(',')}`, regions };\n}\n"],"mappings":";;;;AA+BA,SAAgB,UACf,OACA,MACA,MACiB;CACjB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,QAAQ,KAAK,GAAG,CAAC;CAC3D,MAAM,MAAM,KAAK,IAAI,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,KAAK,QAAQ,CAAC,CAAC;CAC9E,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,QAAQ,OAAO,KAAK,CAAC;CAEhE,OAAO,cAAc,OAAO,KAAK,CAAC,CAAC,KAAK,MAAM,UAAU;EACvD,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,gBAAgB,IAAI,IAAI,KAAK,MAAM,CAAC;EAE1E,OAAO;GACN;GACA,QAAQ,QAAQ,KAAK;GACrB;GACA,GAAG,QAAQ;GACX,GAAG,KAAK,OAAO,KAAK,SAAS,UAAU,CAAC;EACzC;CACD,CAAC;AACF;AAEA,SAAgB,YAAY,MAAmB,OAAoC;CAClF,IAAI,SAAS,GAAG,OAAO;CAEvB,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,SAAS,KAAK,CAAC;CAE1D,OAAO;EACN;EACA,QAAQ,SAAS;EACjB,OAAO,KAAK;EACZ,GAAG;EACH,GAAG,KAAK,OAAO,KAAK,SAAS,UAAU,CAAC;CACzC;AACD;AAEA,SAAgB,aAAa,OAAoB,SAA+B;CAC/E,MAAM,MAAM,eACX,KAAK,OAAO,OAAO,SAAS,UAAU,IAAI,aAAa,KAAK,OAAO;CACpE,MAAM,WAAW,MAAM,SACrB,KAAK,CAAC,OAAO,SAA2B,CAAC,GAAG,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAC7D,QAAQ,CAAC,OAAO,SAAS,MAAM,KAAK;CACtC,MAAM,SAAS,GAAG,MAAM,MAAM;CAC9B,MAAM,QAAQ,MAAM,UAAU,SAAY,SAAS,GAAG,MAAM,KAAK;CACjE,MAAM,UAAmC,SAAS,KAAK,CAAC,MAAM,SAAS;EACtE;EACA,MAAM;EACN;CACD,EAAE;CAEF,IAAI,SAAS,GAAG,QAAQ,KAAK;EAAE,MAAM;EAAG,MAAM;EAAU,IAAI;CAAO,CAAC;CACpE,IAAI,UAAU,QACb,QAAQ,KAAK;EAAE,MAAM,KAAK,IAAI,QAAQ,KAAK;EAAG,MAAM;EAAS,IAAI,KAAK,IAAI,QAAQ,KAAK;CAAE,CAAC;CAG3F,OAAO;EAAE,KAAK,GAAG,OAAO,MAAM,EAAE,GAAG,OAAO,KAAK,EAAE,GAAG,SAAS,KAAK,GAAG;EAAK;CAAQ;AACnF"}
1
+ {"version":3,"file":"strip-scene.js","names":[],"sources":["../../../src/lib/strip-scene.ts"],"sourcesContent":["import type { SurfaceSize } from '#lib/canvas-surface.ts';\n\nimport { clampProportion } from '#lib/math.ts';\nimport { resamplePeaks } from '#lib/resample-peaks.ts';\n\ninterface BarRect {\n\theight: number;\n\tradius: number;\n\twidth: number;\n\tx: number;\n\ty: number;\n}\n\ninterface BarGrid {\n\tgapRatio: number;\n\tpitch: number;\n\tradiusRatio: number;\n}\n\ninterface RegionState {\n\tbuffered: ReadonlyArray<[number, number]>;\n\tplayed: number;\n\tscrub?: number;\n}\n\nexport interface StripRegions {\n\tkey: string;\n\tregions: Array<{ from: number; kind: 'buffered' | 'played' | 'scrub'; to: number }>;\n}\n\n// Whole device pixels, or bars alias unevenly and Skia's CPU raster crawls\nexport function stripBars(\n\tpeaks: ArrayLike<number>,\n\tsize: SurfaceSize,\n\tgrid: BarGrid,\n): Array<BarRect> {\n\tconst pitch = Math.max(1, Math.round(grid.pitch * size.dpr));\n\tconst gap = Math.min(pitch - 1, Math.max(0, Math.round(pitch * grid.gapRatio)));\n\tconst width = pitch - gap;\n\tconst count = Math.max(0, Math.floor((size.width + gap) / pitch));\n\n\treturn resamplePeaks(peaks, count).map((peak, index) => {\n\t\tconst height = Math.max(1, Math.round(clampProportion(peak) * size.height));\n\n\t\treturn {\n\t\t\theight,\n\t\t\tradius: width * grid.radiusRatio,\n\t\t\twidth,\n\t\t\tx: index * pitch,\n\t\t\ty: Math.round((size.height - height) / 2),\n\t\t};\n\t});\n}\n\nexport function stripGroove(\n\tsize: SurfaceSize,\n\t{ grooveRatio, grooveSize }: { grooveRatio: number; grooveSize: number },\n): BarRect | undefined {\n\tconst thickness = grooveSize > 0 ? grooveSize * size.dpr : size.height * grooveRatio;\n\tif (thickness <= 0) return undefined;\n\n\tconst height = Math.max(1, Math.round(Math.min(thickness, size.height)));\n\n\treturn {\n\t\theight,\n\t\tradius: height / 2,\n\t\twidth: size.width,\n\t\tx: 0,\n\t\ty: Math.round((size.height - height) / 2),\n\t};\n}\n\nexport function stripRegions(state: RegionState, widthPx: number): StripRegions {\n\tconst px = (proportion: number): number =>\n\t\tMath.round((Number.isFinite(proportion) ? proportion : 0) * widthPx);\n\tconst buffered = state.buffered\n\t\t.map(([start, end]): [number, number] => [px(start), px(end)])\n\t\t.filter(([start, end]) => end > start);\n\tconst played = px(state.played);\n\tconst scrub = state.scrub === undefined ? played : px(state.scrub);\n\tconst regions: StripRegions['regions'] = buffered.map(([from, to]) => ({\n\t\tfrom,\n\t\tkind: 'buffered',\n\t\tto,\n\t}));\n\n\tif (played > 0) regions.push({ from: 0, kind: 'played', to: played });\n\tif (scrub !== played) {\n\t\tregions.push({ from: Math.min(played, scrub), kind: 'scrub', to: Math.max(played, scrub) });\n\t}\n\n\treturn { key: `${String(played)}:${String(scrub)}:${buffered.join(',')}`, regions };\n}\n"],"mappings":";;;;AA+BA,SAAgB,UACf,OACA,MACA,MACiB;CACjB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,QAAQ,KAAK,GAAG,CAAC;CAC3D,MAAM,MAAM,KAAK,IAAI,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,KAAK,QAAQ,CAAC,CAAC;CAC9E,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,QAAQ,OAAO,KAAK,CAAC;CAEhE,OAAO,cAAc,OAAO,KAAK,CAAC,CAAC,KAAK,MAAM,UAAU;EACvD,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,gBAAgB,IAAI,IAAI,KAAK,MAAM,CAAC;EAE1E,OAAO;GACN;GACA,QAAQ,QAAQ,KAAK;GACrB;GACA,GAAG,QAAQ;GACX,GAAG,KAAK,OAAO,KAAK,SAAS,UAAU,CAAC;EACzC;CACD,CAAC;AACF;AAEA,SAAgB,YACf,MACA,EAAE,aAAa,cACO;CACtB,MAAM,YAAY,aAAa,IAAI,aAAa,KAAK,MAAM,KAAK,SAAS;CACzE,IAAI,aAAa,GAAG,OAAO;CAE3B,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,IAAI,WAAW,KAAK,MAAM,CAAC,CAAC;CAEvE,OAAO;EACN;EACA,QAAQ,SAAS;EACjB,OAAO,KAAK;EACZ,GAAG;EACH,GAAG,KAAK,OAAO,KAAK,SAAS,UAAU,CAAC;CACzC;AACD;AAEA,SAAgB,aAAa,OAAoB,SAA+B;CAC/E,MAAM,MAAM,eACX,KAAK,OAAO,OAAO,SAAS,UAAU,IAAI,aAAa,KAAK,OAAO;CACpE,MAAM,WAAW,MAAM,SACrB,KAAK,CAAC,OAAO,SAA2B,CAAC,GAAG,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAC7D,QAAQ,CAAC,OAAO,SAAS,MAAM,KAAK;CACtC,MAAM,SAAS,GAAG,MAAM,MAAM;CAC9B,MAAM,QAAQ,MAAM,UAAU,SAAY,SAAS,GAAG,MAAM,KAAK;CACjE,MAAM,UAAmC,SAAS,KAAK,CAAC,MAAM,SAAS;EACtE;EACA,MAAM;EACN;CACD,EAAE;CAEF,IAAI,SAAS,GAAG,QAAQ,KAAK;EAAE,MAAM;EAAG,MAAM;EAAU,IAAI;CAAO,CAAC;CACpE,IAAI,UAAU,QACb,QAAQ,KAAK;EAAE,MAAM,KAAK,IAAI,QAAQ,KAAK;EAAG,MAAM;EAAS,IAAI,KAAK,IAAI,QAAQ,KAAK;CAAE,CAAC;CAG3F,OAAO;EAAE,KAAK,GAAG,OAAO,MAAM,EAAE,GAAG,OAAO,KAAK,EAAE,GAAG,SAAS,KAAK,GAAG;EAAK;CAAQ;AACnF"}
@@ -0,0 +1,20 @@
1
+ import { clamp } from "./math.js";
2
+
3
+ //#region src/lib/toggle-travel.ts
4
+ function travelFrom({ at, count, first, isOnCap, last, pointer, well }) {
5
+ const step = count < 2 ? 0 : (well.length - well.thickness) / (count - 1);
6
+ return {
7
+ first,
8
+ last,
9
+ origin: isOnCap ? pointer - at * step : well.start + well.thickness / 2,
10
+ step
11
+ };
12
+ }
13
+ function positionAt({ first, last, origin, step }, pointer) {
14
+ if (step <= 0) return first;
15
+ return clamp((pointer - origin) / step, first, last);
16
+ }
17
+
18
+ //#endregion
19
+ export { positionAt, travelFrom };
20
+ //# sourceMappingURL=toggle-travel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle-travel.js","names":[],"sources":["../../../src/lib/toggle-travel.ts"],"sourcesContent":["import { clamp } from '#lib/math.ts';\n\ninterface Well {\n\tlength: number;\n\tstart: number;\n\tthickness: number;\n}\n\ninterface Grab {\n\tat: number;\n\tcount: number;\n\tfirst: number;\n\tisOnCap: boolean;\n\tlast: number;\n\tpointer: number;\n\twell: Well;\n}\n\nexport interface ToggleTravel {\n\tfirst: number;\n\tlast: number;\n\torigin: number;\n\tstep: number;\n}\n\nexport function travelFrom({ at, count, first, isOnCap, last, pointer, well }: Grab): ToggleTravel {\n\tconst step = count < 2 ? 0 : (well.length - well.thickness) / (count - 1);\n\tconst origin = isOnCap ? pointer - at * step : well.start + well.thickness / 2;\n\n\treturn { first, last, origin, step };\n}\n\nexport function positionAt({ first, last, origin, step }: ToggleTravel, pointer: number): number {\n\tif (step <= 0) return first;\n\n\treturn clamp((pointer - origin) / step, first, last);\n}\n"],"mappings":";;;AAyBA,SAAgB,WAAW,EAAE,IAAI,OAAO,OAAO,SAAS,MAAM,SAAS,QAA4B;CAClG,MAAM,OAAO,QAAQ,IAAI,KAAK,KAAK,SAAS,KAAK,cAAc,QAAQ;CAGvE,OAAO;EAAE;EAAO;EAAM,QAFP,UAAU,UAAU,KAAK,OAAO,KAAK,QAAQ,KAAK,YAAY;EAE/C;CAAK;AACpC;AAEA,SAAgB,WAAW,EAAE,OAAO,MAAM,QAAQ,QAAsB,SAAyB;CAChG,IAAI,QAAQ,GAAG,OAAO;CAEtB,OAAO,OAAO,UAAU,UAAU,MAAM,OAAO,IAAI;AACpD"}
@@ -84,7 +84,7 @@
84
84
  transition-delay: var(--sonic-button-busy-delay, 0s);
85
85
  }
86
86
 
87
- .sonic-button:active:where(:not(:disabled, [aria-disabled='true'])),
87
+ .sonic-button:is(:active, [data-sonic-pressed]):where(:not(:disabled, [aria-disabled='true'])),
88
88
  .sonic-button:is([aria-pressed='true'], :where([momentary]:state(pressed)) > *) {
89
89
  --_sonic-keycap-shift: calc(var(--_sonic-keycap-depth) / 2);
90
90
  --_sonic-keycap-scale: var(--_sonic-button-press-scale);
@@ -126,7 +126,7 @@
126
126
  [aria-disabled='true'],
127
127
  :where([momentary]:state(pressed), [armed]) > *,
128
128
  :where([busy]) > :not(:has(.sonic-led))
129
- ):is(:focus-visible, :active) {
129
+ ):is(:focus-visible, :active, [data-sonic-pressed]) {
130
130
  color: var(--_sonic-ink-lift);
131
131
  }
132
132
 
@@ -4,6 +4,7 @@
4
4
  @import './button.css';
5
5
  @import './led.css';
6
6
  @import './switch.css';
7
+ @import './toggle.css';
7
8
  @import './meter.css';
8
9
  @import './number.css';
9
10
  @import './panel.css';
@@ -8,7 +8,6 @@
8
8
  .sonic-led {
9
9
  --_sonic-led-size: var(--sonic-led-size, 0.5rem);
10
10
  --_sonic-led-lens-ratio: var(--sonic-led-lens-ratio, calc(0.8 - 0.175 * var(--_sonic-relief)));
11
- --_sonic-lit-alt: var(--sonic-lit-alt, oklch(79% 0.2 150deg));
12
11
  --_sonic-unit: calc(var(--_sonic-led-size) / 8);
13
12
  --_sonic-led-modelled: calc(100% * var(--_sonic-relief));
14
13
  --_sonic-led-bezel: var(
@@ -45,9 +44,9 @@
45
44
  }
46
45
 
47
46
  @media (forced-colors: active) {
48
- --_sonic-lit-alt: Mark;
49
- --_sonic-hot: Highlight;
50
- --_sonic-clip: Mark;
47
+ --_sonic-lit-ok: Mark;
48
+ --_sonic-lit-warning: Highlight;
49
+ --_sonic-lit-danger: Mark;
51
50
 
52
51
  forced-color-adjust: none;
53
52
  background: none;
@@ -59,21 +58,21 @@
59
58
  }
60
59
  }
61
60
 
62
- .sonic-led[data-sonic-lit='alt'] {
63
- --_sonic-lit: var(--_sonic-lit-alt);
61
+ .sonic-led[data-sonic-lit='ok'] {
62
+ --_sonic-lit: var(--_sonic-lit-ok);
64
63
  }
65
64
 
66
- .sonic-led[data-sonic-lit='hot'] {
67
- --_sonic-lit: var(--_sonic-hot);
65
+ .sonic-led[data-sonic-lit='warning'] {
66
+ --_sonic-lit: var(--_sonic-lit-warning);
68
67
  }
69
68
 
70
- .sonic-led[data-sonic-lit='clip'] {
71
- --_sonic-lit: var(--_sonic-clip);
69
+ .sonic-led[data-sonic-lit='danger'] {
70
+ --_sonic-lit: var(--_sonic-lit-danger);
72
71
  }
73
72
 
74
73
  /* Before the lit rule, which a latched button's LED also matches */
75
74
  .sonic-led:is(
76
- [data-sonic-lit='dim'],
75
+ [data-sonic-lit='idle'],
77
76
  :where(:is([armed], [busy]) > .sonic-button:not(:disabled, [aria-disabled='true'])) *
78
77
  ) {
79
78
  &::after {
@@ -91,11 +90,15 @@
91
90
  }
92
91
 
93
92
  .sonic-led:is(
94
- [data-sonic-lit]:where(:not([data-sonic-lit='dim'])),
93
+ [data-sonic-lit]:where(:not([data-sonic-lit='idle'])),
95
94
  :where(
96
95
  .sonic-button[aria-pressed='true'],
97
96
  [momentary]:state(pressed) > .sonic-button,
98
- .sonic-segmented-option[aria-checked='true']
97
+ .sonic-segmented-option[aria-checked='true'],
98
+ .sonic-toggle:has(
99
+ > [role='switch'][aria-checked='true'],
100
+ > [aria-checked='true'] [data-sonic-lit]
101
+ )
99
102
  )
100
103
  *,
101
104
  :where(:state(at-origin):not(:disabled) > .sonic-led)
@@ -31,6 +31,7 @@
31
31
  .sonic-button,
32
32
  .sonic-led,
33
33
  .sonic-switch,
34
+ .sonic-toggle,
34
35
  .sonic-meter,
35
36
  .sonic-number,
36
37
  .sonic-panel,
@@ -75,6 +76,7 @@
75
76
  .sonic-segmented-option,
76
77
  .sonic-slider,
77
78
  .sonic-spectrum,
79
+ .sonic-toggle,
78
80
  .sonic-waveform,
79
81
  .sonic-wavestrip,
80
82
  .sonic-xy
@@ -111,15 +113,17 @@
111
113
  --_sonic-lens: var(--_sonic-lens-glint), var(--_sonic-lens-glass);
112
114
  }
113
115
 
114
- :is(.sonic-led, .sonic-meter, .sonic-spectrum) {
115
- --_sonic-hot: var(--sonic-hot, oklch(70.5% 0.187 48deg));
116
- --_sonic-clip: var(--sonic-clip, oklch(63.7% 0.208 25deg));
116
+ :is(.sonic-led, .sonic-meter, .sonic-spectrum, .sonic-toggle) {
117
+ --_sonic-lit-ok: var(--sonic-lit-ok, oklch(79% 0.2 150deg));
118
+ --_sonic-lit-warning: var(--sonic-lit-warning, oklch(70.5% 0.187 48deg));
119
+ --_sonic-lit-danger: var(--sonic-lit-danger, oklch(63.7% 0.208 25deg));
117
120
  }
118
121
 
119
122
  :is(
120
123
  .sonic-dial,
121
124
  .sonic-button,
122
125
  .sonic-switch,
126
+ .sonic-toggle,
123
127
  .sonic-number,
124
128
  .sonic-segmented-option,
125
129
  .sonic-slider,
@@ -359,6 +363,7 @@
359
363
  .sonic-screen,
360
364
  .sonic-segmented,
361
365
  .sonic-spectrum,
366
+ .sonic-toggle,
362
367
  .sonic-waveform,
363
368
  .sonic-wavestrip,
364
369
  .sonic-xy
@@ -374,7 +379,7 @@
374
379
  rgb(255 255 255 / calc(0.06 * var(--_sonic-relief)));
375
380
  }
376
381
 
377
- :is(.sonic-button, .sonic-segmented) {
382
+ :is(.sonic-button, .sonic-segmented, .sonic-toggle) {
378
383
  --_sonic-well-floor: var(
379
384
  --sonic-well,
380
385
  linear-gradient(
@@ -402,7 +407,7 @@
402
407
  }
403
408
 
404
409
  /* On the option rather than the strip, where forced colours clear the drop shade */
405
- :is(.sonic-button, .sonic-segmented-option) {
410
+ :is(.sonic-button, .sonic-segmented-option, .sonic-toggle) {
406
411
  --_sonic-press-drop: calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y))
407
412
  var(--_sonic-unit) var(--_sonic-drop-shade);
408
413
  --_sonic-keycap-shift: calc(var(--_sonic-keycap-depth) / -2);
@@ -419,9 +424,23 @@
419
424
  0 0 var(--_sonic-unit)
420
425
  color-mix(in oklab, var(--_sonic-lit) calc(24% * var(--_sonic-relief)), transparent)
421
426
  );
427
+ --_sonic-legend-lit-from: var(--_sonic-lit);
428
+ --_sonic-legend-lit: color-mix(
429
+ in oklab,
430
+ var(--sonic-cap) 0%,
431
+ oklch(
432
+ from
433
+ color-mix(
434
+ in oklab,
435
+ oklch(from var(--_sonic-ink) l c h / clamp(0, (0.6 - l) * 1000, 1)),
436
+ oklch(from var(--_sonic-legend-lit-from) l c h / 0.1%)
437
+ )
438
+ l c h / 100%
439
+ )
440
+ );
422
441
  }
423
442
 
424
- :is(.sonic-button, .sonic-segmented-option, .sonic-switch-position) {
443
+ :is(.sonic-button, .sonic-segmented-option, .sonic-switch-position, .sonic-toggle-position) {
425
444
  --_sonic-target-outset: min(0px, calc((100% - var(--sonic-target-size, 100%)) / 2));
426
445
 
427
446
  &::after {
@@ -432,7 +451,7 @@
432
451
  }
433
452
 
434
453
  /* Each sheet maps its own depth, corner, bevel and face corner onto the `keycap` tokens */
435
- :is(.sonic-button-cap, .sonic-segmented-cap) {
454
+ :is(.sonic-button-cap, .sonic-segmented-cap, .sonic-toggle-cap) {
436
455
  display: grid;
437
456
  place-items: center;
438
457
  background: var(--_sonic-skirt);
@@ -679,7 +698,8 @@
679
698
  .sonic-number,
680
699
  .sonic-ring,
681
700
  .sonic-segmented-option,
682
- .sonic-slider
701
+ .sonic-slider,
702
+ .sonic-toggle
683
703
  ) {
684
704
  --_sonic-lit: Highlight;
685
705
  --_sonic-unlit: GrayText;
@@ -692,6 +712,7 @@
692
712
  .sonic-number,
693
713
  .sonic-segmented-option,
694
714
  .sonic-slider,
715
+ .sonic-toggle,
695
716
  .sonic-waveform,
696
717
  .sonic-wavestrip,
697
718
  .sonic-xy
@@ -737,7 +758,13 @@
737
758
  }
738
759
  }
739
760
 
740
- :is(.sonic-dial-cap, .sonic-button-cap, .sonic-segmented-cap, .sonic-slider-cap) {
761
+ :is(
762
+ .sonic-dial-cap,
763
+ .sonic-button-cap,
764
+ .sonic-segmented-cap,
765
+ .sonic-slider-cap,
766
+ .sonic-toggle-cap
767
+ ) {
741
768
  color: var(--_sonic-ink);
742
769
  forced-color-adjust: none;
743
770
  }
@@ -290,8 +290,9 @@
290
290
  background: linear-gradient(
291
291
  var(--_sonic-meter-toward),
292
292
  color-mix(in oklab, var(--_sonic-lit) 12%, var(--_sonic-unlit)) 0 var(--_sonic-meter-hot-at),
293
- color-mix(in oklab, var(--_sonic-hot) 12%, var(--_sonic-unlit)) 0 var(--_sonic-meter-clip-at),
294
- color-mix(in oklab, var(--_sonic-clip) 12%, var(--_sonic-unlit)) 0
293
+ color-mix(in oklab, var(--_sonic-lit-warning) 12%, var(--_sonic-unlit)) 0
294
+ var(--_sonic-meter-clip-at),
295
+ color-mix(in oklab, var(--_sonic-lit-danger) 12%, var(--_sonic-unlit)) 0
295
296
  );
296
297
  }
297
298
 
@@ -318,8 +319,8 @@
318
319
  background: linear-gradient(
319
320
  var(--_sonic-meter-toward),
320
321
  var(--_sonic-lit) 0 var(--_sonic-meter-hot-at),
321
- var(--_sonic-hot) 0 var(--_sonic-meter-clip-at),
322
- var(--_sonic-clip) 0
322
+ var(--_sonic-lit-warning) 0 var(--_sonic-meter-clip-at),
323
+ var(--_sonic-lit-danger) 0
323
324
  );
324
325
  mask:
325
326
  linear-gradient(
@@ -347,8 +348,8 @@
347
348
  content: '';
348
349
  background: radial-gradient(
349
350
  closest-side,
350
- color-mix(in oklab, var(--_sonic-clip) 55%, #fff),
351
- var(--_sonic-clip)
351
+ color-mix(in oklab, var(--_sonic-lit-danger) 55%, #fff),
352
+ var(--_sonic-lit-danger)
352
353
  );
353
354
  border-radius: inherit;
354
355
  opacity: var(--_sonic-meter-clipped, 0);