@xsynaptic/sonic-ui 0.17.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 (125) hide show
  1. package/CHANGELOG.md +13 -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 +42 -241
  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/peaks-request.js +44 -0
  27. package/dist/default/lib/positions.js +58 -0
  28. package/dist/default/lib/scrub.js +12 -0
  29. package/dist/default/lib/value-model.js +62 -0
  30. package/dist/default/lib/waveform-scene.js +108 -0
  31. package/dist/default/lib/zoom-gesture.js +97 -1
  32. package/dist/dev/dat.d.ts +1 -0
  33. package/dist/dev/dat.js +1 -0
  34. package/dist/dev/dat.js.map +1 -1
  35. package/dist/dev/elements/button.d.ts +2 -0
  36. package/dist/dev/elements/button.js +2 -0
  37. package/dist/dev/elements/button.js.map +1 -1
  38. package/dist/dev/elements/dial.d.ts +2 -0
  39. package/dist/dev/elements/dial.js +2 -0
  40. package/dist/dev/elements/dial.js.map +1 -1
  41. package/dist/dev/elements/display-element.d.ts +5 -3
  42. package/dist/dev/elements/display-element.js +9 -7
  43. package/dist/dev/elements/display-element.js.map +1 -1
  44. package/dist/dev/elements/envelope.js +31 -72
  45. package/dist/dev/elements/envelope.js.map +1 -1
  46. package/dist/dev/elements/field-gesture.js +82 -0
  47. package/dist/dev/elements/field-gesture.js.map +1 -0
  48. package/dist/dev/elements/meter.d.ts +10 -5
  49. package/dist/dev/elements/meter.js +4 -0
  50. package/dist/dev/elements/meter.js.map +1 -1
  51. package/dist/dev/elements/position-group.d.ts +12 -6
  52. package/dist/dev/elements/position-group.js +42 -26
  53. package/dist/dev/elements/position-group.js.map +1 -1
  54. package/dist/dev/elements/radio-group.d.ts +11 -7
  55. package/dist/dev/elements/radio-group.js +59 -56
  56. package/dist/dev/elements/radio-group.js.map +1 -1
  57. package/dist/dev/elements/scale.js +3 -3
  58. package/dist/dev/elements/scale.js.map +1 -1
  59. package/dist/dev/elements/slider.d.ts +4 -2
  60. package/dist/dev/elements/slider.js +11 -13
  61. package/dist/dev/elements/slider.js.map +1 -1
  62. package/dist/dev/elements/split.d.ts +1 -0
  63. package/dist/dev/elements/split.js +14 -13
  64. package/dist/dev/elements/split.js.map +1 -1
  65. package/dist/dev/elements/switch.d.ts +2 -2
  66. package/dist/dev/elements/switch.js +31 -46
  67. package/dist/dev/elements/switch.js.map +1 -1
  68. package/dist/dev/elements/toggle.js +20 -44
  69. package/dist/dev/elements/toggle.js.map +1 -1
  70. package/dist/dev/elements/value-element.d.ts +5 -5
  71. package/dist/dev/elements/value-element.js +65 -91
  72. package/dist/dev/elements/value-element.js.map +1 -1
  73. package/dist/dev/elements/value-gestures.js +22 -22
  74. package/dist/dev/elements/value-gestures.js.map +1 -1
  75. package/dist/dev/elements/wave-element.d.ts +6 -4
  76. package/dist/dev/elements/wave-element.js +9 -12
  77. package/dist/dev/elements/wave-element.js.map +1 -1
  78. package/dist/dev/elements/waveform.d.ts +19 -21
  79. package/dist/dev/elements/waveform.js +42 -241
  80. package/dist/dev/elements/waveform.js.map +1 -1
  81. package/dist/dev/elements/wavestrip.d.ts +5 -1
  82. package/dist/dev/elements/wavestrip.js +10 -5
  83. package/dist/dev/elements/wavestrip.js.map +1 -1
  84. package/dist/dev/elements/xy.js +67 -112
  85. package/dist/dev/elements/xy.js.map +1 -1
  86. package/dist/dev/index.d.ts +2 -1
  87. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  88. package/dist/dev/lib/canvas-surface.js +25 -0
  89. package/dist/dev/lib/canvas-surface.js.map +1 -1
  90. package/dist/dev/lib/crossfade.d.ts +1 -0
  91. package/dist/dev/lib/crossfade.js +1 -0
  92. package/dist/dev/lib/crossfade.js.map +1 -1
  93. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  94. package/dist/dev/lib/envelope-curve.js +1 -0
  95. package/dist/dev/lib/envelope-curve.js.map +1 -1
  96. package/dist/dev/lib/peaks-request.d.ts +5 -0
  97. package/dist/dev/lib/peaks-request.js +45 -0
  98. package/dist/dev/lib/peaks-request.js.map +1 -0
  99. package/dist/dev/lib/positions.d.ts +7 -0
  100. package/dist/dev/lib/positions.js +59 -0
  101. package/dist/dev/lib/positions.js.map +1 -0
  102. package/dist/dev/lib/scrub.d.ts +8 -0
  103. package/dist/dev/lib/scrub.js +13 -0
  104. package/dist/dev/lib/scrub.js.map +1 -0
  105. package/dist/dev/lib/time-regions.d.ts +1 -0
  106. package/dist/dev/lib/time-regions.js.map +1 -1
  107. package/dist/dev/lib/units.d.ts +8 -0
  108. package/dist/dev/lib/value-model.js +63 -0
  109. package/dist/dev/lib/value-model.js.map +1 -0
  110. package/dist/dev/lib/waveform-buckets.d.ts +1 -0
  111. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  112. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  113. package/dist/dev/lib/waveform-scene.js +109 -0
  114. package/dist/dev/lib/waveform-scene.js.map +1 -0
  115. package/dist/dev/lib/zoom-gesture.js +97 -1
  116. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  117. package/dist/styles/button.css +16 -6
  118. package/dist/styles/waveform.css +2 -1
  119. package/package.json +1 -1
  120. package/dist/default/lib/asked-value.js +0 -21
  121. package/dist/default/lib/fill-watch.js +0 -29
  122. package/dist/dev/lib/asked-value.js +0 -22
  123. package/dist/dev/lib/asked-value.js.map +0 -1
  124. package/dist/dev/lib/fill-watch.js +0 -30
  125. package/dist/dev/lib/fill-watch.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - - Button: an icon keeps its `viewBox` proportions at `--sonic-button-icon-ratio` of the button's height, and the button widens to fit it, never under `--sonic-button-aspect-ratio`; at 0 the button is as wide as its icon. Breaking for an icon wider than it is tall, which was shrunk into a square.
8
+ - Waveform: a dimmed marker's label dims by opacity, so `renderLabel` children with their own colour dim too, as does the label's scrim. Drop any rule that dimmed them by hand.
9
+
10
+ ### Patch Changes
11
+
12
+ - Internals restructured into smaller modules; no change to the public surface.
13
+ - Switch and toggle internals restructured around position indices; no change to the public surface.
14
+ - The types say the unit or the contract on the properties whose type alone does not, such as a meter's `level` and a waveform's `zoom`.
15
+
3
16
  ## 0.17.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -4,6 +4,14 @@ Native custom elements for audio interfaces. Each control renders in the light D
4
4
 
5
5
  Early and unstable: any 0.x release may break the API.
6
6
 
7
+ This README holds what the package's own files do not say. For the rest, read the package:
8
+
9
+ - Properties and methods: `dist/dev/elements/<control>.d.ts`. Every attribute has a property of the same name in camel case.
10
+ - Tokens and their defaults: `dist/styles/<control>.css` and `dist/styles/material/`, where each token is read as `var(--sonic-…, default)`.
11
+ - Working markup for every control: `playground/src/components/specimens/` in the repository.
12
+
13
+ ## Setup
14
+
7
15
  ```sh
8
16
  pnpm add @xsynaptic/sonic-ui
9
17
  ```
@@ -20,12 +28,111 @@ import '@xsynaptic/sonic-ui/define';
20
28
  ```
21
29
 
22
30
  ```html
23
- <sonic-dial aria-label="Cutoff" value="40"></sonic-dial>
31
+ <body class="sonic-skin-slate">
32
+ <sonic-dial aria-label="Cutoff" value="40"></sonic-dial>
33
+ </body>
34
+ ```
35
+
36
+ - Every sheet sits in the `sonic` layer, so declare it below the layer your own rules live in.
37
+ - A skin sheet does nothing until its class, `sonic-skin-<name>`, is on an ancestor of the controls. The skins are `slate`, `amber`, `ivory`, `lime` and `flat`; a wrapper with another skin's class reskins what is inside it. With no skin a control is bare: no cap and no well, drawn in the text colour around it.
38
+ - `./define` registers every control. `./define/<control>` registers one, and `.` exports the classes and registers nothing.
39
+ - Under a bundler that sets the `development` condition, as Vite's dev server does, the controls warn in the console about a missing sheet, a child they do not use and a binding that found nothing. Read the console before debugging.
40
+
41
+ ## What every control shares
42
+
43
+ - The host is `display: contents` and the control is an element inside it. A width, margin or transform on the host does nothing: size a control with its token (`--sonic-dial-size`, `--sonic-slider-length`), which can be set on the host or any ancestor, and place it with its parent's layout.
44
+ - Name every control with `aria-label` or `aria-labelledby` on the host, or with a `<label>` for a form-associated one; the control moves the name onto its focusable part.
45
+ - A boolean attribute is on when present, whatever its value, so `pressed="false"` latches a button. From a framework pass `undefined`, never `false`.
46
+ - A control uses only the children it documents below, and adds one child of its own. Under a hydrating framework, register the definitions once hydration is done (in React, from an effect), so the added child is no mismatch.
47
+ - Events bubble and carry no detail: read the property when one fires. A control knows nothing about playback, so wire `input` and `change` to your own state and write `value` back.
48
+ - Buttons, value controls, the segmented control, the switch, the toggle and the XY pad are form-associated: give one a `name` and it submits, resets and restores with its form.
49
+ - States a control sets itself are custom states on the host, selected with `:state()`: `dragging`, `cancelling`, `editing`, `revealed`, `springing`, `modulated`, `pressed`, `held`, `ladder`, `empty` and `pending`.
50
+
51
+ ## Value controls
52
+
53
+ The dial, the slider, the number box and both wave controls hold one number, from `min` (0) to `max` (100) in steps of `step` (1).
54
+
55
+ - `input` fires as the value moves and `change` once when a gesture ends, however it ends.
56
+ - A drag owns the value: a write to `value` during one is dropped, so a seek bar fed by `timeupdate` needs no guard of its own.
57
+ - A double press, or Enter, opens typed entry. `double-press="reset"` makes it go back to `default` instead. Cmd-click on Apple platforms, Ctrl-click elsewhere, and Delete reset whichever is chosen, once `default` is set.
58
+ - `formatValue` and `parseValue` are properties holding functions. What the first writes, the second has to read, or typed entry opens on text it cannot take; `formatEntry` covers the case where it cannot.
59
+ - The readout bubble is opt-in, with `readout`. Assistive technology hears the value without it; `formatSpokenValue` words it.
60
+ - `taper="log"` gives even octaves on a range above zero, and `midpoint` bends a range that starts at zero. On a tapered control the arrow keys move by travel; `key-step` makes them move by value.
61
+ - `positions` is a property holding the numbers a stepped control stops at, spaced evenly whatever their values; their labels come from `formatValue`.
62
+ - `modulationValue` is a property only, meant to be written every frame. It moves the light and fires nothing.
63
+ - To take a key from a control, call `preventDefault()` in a capture listener.
64
+
65
+ ## Press controls
66
+
67
+ A button's children are its legend, an icon or a label, always an element. They are copied into the cap, so the copy has none of your listeners and any id in it is prefixed; keep them static. A child with `data-sonic-when="pressed"` or `"released"` shows only in that state.
68
+
69
+ ```html
70
+ <sonic-button aria-label="Loop" latching>
71
+ <span class="sonic-led"></span>
72
+ <svg aria-hidden="true">…</svg>
73
+ </sonic-button>
24
74
  ```
25
75
 
26
- A display takes its signal as data: `spectrum.analyser = node`, or `spectrum.push(frame)`.
76
+ - A plain button fires `click`. `latching` makes it stay pressed, with `pressed` and a `change` event; `momentary` makes it pressed for as long as it is held, and acts on the press, not the release.
77
+ - `soft-disabled` stops presses but keeps the tab stop, for a button that would otherwise lose focus when it disables itself. `busy` leaves it pressable.
78
+ - The segmented control, the switch and the toggle take one child per position, each with `data-sonic-value`, and hold the chosen one in `value`. They fire `change` only. A switch or a toggle with no children is on or off, held in `checked`.
79
+ - On a position child, `data-sonic-disabled` locks it, `data-sonic-momentary` makes an end position spring back, and on a toggle `data-sonic-lit` lights the well there (`ok`, `warning` or `danger` for the colour). A lock stops the user, not your code: a `value` written to a disabled position is chosen.
80
+
81
+ ```html
82
+ <sonic-segmented aria-label="Filter" value="lp">
83
+ <span data-sonic-value="lp">LP</span>
84
+ <span data-sonic-value="bp">BP</span>
85
+ <span data-sonic-value="hp">HP</span>
86
+ </sonic-segmented>
87
+ ```
88
+
89
+ ## Parts that are only a class
90
+
91
+ The LED, the panel, the ring and the screen have no behaviour, so they are classes on your own elements and need no registration.
92
+
93
+ - `<span class="sonic-led">` is unlit until it carries `data-sonic-lit`, whose value picks the colour: empty, `idle`, `ok`, `warning` or `danger`. Inside a latching button or a toggle's cap it lights with the control.
94
+ - `class="sonic-ring"` draws a lit arc around its one child, from `--sonic-ring-from` to `--sonic-ring-to`, each 0 to 1. Give it your own `role="progressbar"`.
95
+ - `class="sonic-screen"` is a pane of glass for your own text or canvas, and `class="sonic-panel"` groups controls under a `sonic-panel-title`.
96
+
97
+ ## Displays
98
+
99
+ A display takes its signal as data and never opens an `AudioContext`.
100
+
101
+ - `<sonic-meter>`: write `level`, a linear peak amplitude, as often as you have one; the meter converts to decibels and applies its `ballistics`. Write `value` instead to show a number in the meter's own units exactly as given. `segments` turns the bar into a ladder. It fires nothing and has no role: read `peak`, call `resetPeak()`, and announce numbers yourself.
102
+ - `<sonic-spectrum>`: set `analyser` to an `AnalyserNode`, or call `push(decibels)` with a `Float32Array` holding one level per bin and set `sample-rate`.
103
+
104
+ ## Controls that drive other controls
105
+
106
+ - `<sonic-split>` wraps value controls and keeps their values adding up to `total`. `mode` chooses who gives way (`proportional`, `equal` or `cascade`), and `data-sonic-locked` on a member holds it still. The others move a microtask after the one the user moved, so its `input` arrives first.
107
+ - `<sonic-envelope>` has no value and no events. Each of its attributes (`delay`, `attack`, `hold`, `decay`, `sustain`, `release`, and `attack-curve`, `decay-curve`, `release-curve`) names the id of a dial, a slider or a number box; dragging a handle turns that control, which fires its own `input`. Stages share the width equally, whatever their times.
108
+ - `<sonic-xy>` holds `x` and `y`, each with its own `x-min`, `x-max`, `x-step` and so on, and submits them as `name.x` and `name.y`.
109
+ - `envelopeCurve` and `crossfadeGains` are exported so your DSP computes what the control draws.
110
+
111
+ ## Wave controls
112
+
113
+ `<sonic-wavestrip>` is the overview of a track, scrubbed to seek; `<sonic-waveform>` is the close-up that scrolls under a fixed playhead. Both are value controls in seconds that take their data as properties (`peaks`, `markers`); the types list the rest. `./dat` reads an audiowaveform `.dat` file into `peaks`, and fetches nothing itself.
114
+
115
+ Their events bubble and carry no detail, so read the property when one fires: `revealed` on `sonic-reveal`, `currentMarker` on `sonic-marker`, and the strip's `hoverValue` on `sonic-hover`, which `clientXOf(value)` turns into a viewport x.
116
+
117
+ The strip draws one part per marker inside `.sonic-wavestrip-markers`, in `start` order: a `.sonic-wavestrip-marker` dot, or a `.sonic-wavestrip-region` when the marker has an `end`. Marker labels are hidden from assistive technology; fold `currentMarker` into `formatSpokenValue` to have one spoken.
118
+
119
+ The waveform parks the current marker's label in its corner and fades it after the next marker crosses the playhead, never before: `--sonic-waveform-label-fade-start` (0) and `--sonic-waveform-label-fade-end` (1) run from that crossing, 0, to the next label reaching the park, 1.
120
+
121
+ The waveform calls `requestPeaks(fromSeconds, toSeconds)` as its window moves, often every frame, so return at once for a span already loaded or on its way. Return a promise, or several, and the waveform repaints as each settles; an `async` function is the wrong shape, since it hands back a new settled promise each call. `pending` is yours to keep: list the spans being fetched, and clear each as it lands.
122
+
123
+ With `zoomable`, a ctrl or cmd wheel, a pinch and the `+` and `-` keys set the waveform's `zoom` and fire `sonic-zoom`; a write to `zoom` fires nothing.
124
+
125
+ With `fill` a control takes its container's height and its ratios follow. `--sonic-wavestrip-marker-size`, `--sonic-wavestrip-groove-size` and `--sonic-waveform-label-font-size` hold an absolute length instead.
126
+
127
+ ## Skinning
128
+
129
+ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to start one. Tokens, hook classes (`.sonic-dial`, `.sonic-wavestrip-marker`), `data-sonic-*` attributes and states are the public surface. Anything starting `--_sonic-` is private and changes without notice.
27
130
 
28
- Under a hydrating framework, register the definitions once hydration is done (in React, from an effect); a control owns one child of its host and uses only the children it documents.
131
+ - Set tokens, not rules on hook classes. If a look needs a hook-class rule, a token is missing.
132
+ - Most geometry tokens end in `-ratio` and are a unitless share of the control's size, so one size token scales the whole control.
133
+ - A length token takes any CSS length except a percentage. For a fluid size use container units.
134
+ - `--sonic-relief` runs from 0, flat, to 1, and scales every bevel and shadow at once. `--sonic-light-tilt` turns the one light they all follow.
135
+ - `--sonic-target-size` widens the region that takes a press past the control's box. It is unset by default.
29
136
 
30
137
  ## Loading less
31
138
 
@@ -58,20 +165,6 @@ Under a hydrating framework, register the definitions once hydration is done (in
58
165
  | `wavestrip.css` | core, cap, glass, pane, well, readout, value |
59
166
  | `xy.css` | core, cap, glass, pane, well, readout, bracket |
60
167
 
61
- An LED inside a control also needs `led.css`, and a button inside a ring needs `button.css`. The development build warns about a missing sheet.
62
-
63
- ## Wave controls
64
-
65
- `<sonic-wavestrip>` is the overview of a track, scrubbed to seek; `<sonic-waveform>` is the close-up that scrolls under a fixed playhead. Both are value controls in seconds that take their data as properties (`peaks`, `markers`); the types list the rest.
66
-
67
- Their events bubble and carry no detail, so read the property when one fires: `revealed` on `sonic-reveal`, `currentMarker` on `sonic-marker`, and the strip's `hoverValue` on `sonic-hover`, which `clientXOf(value)` turns into a viewport x.
68
-
69
- The strip draws one part per marker inside `.sonic-wavestrip-markers`, in `start` order: a `.sonic-wavestrip-marker` dot, or a `.sonic-wavestrip-region` when the marker has an `end`. Marker labels are hidden from assistive technology; fold `currentMarker` into `formatSpokenValue` to have one spoken.
70
-
71
- The waveform calls `requestPeaks(fromSeconds, toSeconds)` as its window moves, often every frame, so return at once for a span already loaded or on its way. Return a promise, or several, and the waveform repaints as each settles; an `async` function is the wrong shape, since it hands back a new settled promise each call. `pending` is yours to keep: list the spans being fetched, and clear each as it lands.
72
-
73
- With `zoomable`, a ctrl or cmd wheel, a pinch and the `+` and `-` keys set the waveform's `zoom` and fire `sonic-zoom`; a write to `zoom` fires nothing.
74
-
75
- With `fill` a control takes its container's height and its ratios follow. `--sonic-wavestrip-marker-size`, `--sonic-wavestrip-groove-size` and `--sonic-waveform-label-font-size` hold an absolute length instead.
168
+ An LED inside a control also needs `led.css`, and a button inside a ring needs `button.css`.
76
169
 
77
- Targets current Chrome and Edge, Safari 26 and Firefox 147. MIT licensed.
170
+ Targets current Chrome and Edge, Safari 26 and Firefox 147. No control mirrors for right-to-left. MIT licensed.
@@ -28,6 +28,7 @@ function readDatHeader(buffer) {
28
28
  version
29
29
  };
30
30
  }
31
+ /** An inclusive byte range for a `Range` header; `undefined` when the pairs asked for are empty */
31
32
  function datByteRange(header, fromPair, toPair) {
32
33
  const from = clamp(Math.floor(fromPair), 0, header.pairs);
33
34
  const to = clamp(Math.ceil(toPair), from, header.pairs);
@@ -54,6 +54,7 @@ var SonicButton = class extends SonicFormElement {
54
54
  set latching(isLatching) {
55
55
  this.reflect("latching", isLatching);
56
56
  }
57
+ /** Shows the legend child whose `data-sonic-when` has this name */
57
58
  get legend() {
58
59
  return this.getAttribute("legend") ?? void 0;
59
60
  }
@@ -80,6 +81,7 @@ var SonicButton = class extends SonicFormElement {
80
81
  set softDisabled(isSoftDisabled) {
81
82
  this.reflect("soft-disabled", isSoftDisabled);
82
83
  }
84
+ /** Submitted with the form while a latching button is pressed */
83
85
  get value() {
84
86
  return this.getAttribute("value") ?? "on";
85
87
  }
@@ -37,12 +37,14 @@ var SonicDial = class extends SonicValueElement {
37
37
  set endless(isEndless) {
38
38
  this.reflect("endless", isEndless);
39
39
  }
40
+ /** How far a modulation source can push the value, in value units; signed */
40
41
  get modulation() {
41
42
  return this.numberAttribute("modulation", 0);
42
43
  }
43
44
  set modulation(amount) {
44
45
  this.reflect("modulation", amount);
45
46
  }
47
+ /** Where the value is right now with modulation applied; a property only, cheap to write every frame, fires nothing */
46
48
  get modulationValue() {
47
49
  return this.#modulation.value();
48
50
  }
@@ -1,6 +1,5 @@
1
1
  import { SonicElement } from "./sonic-element.js";
2
2
  import { bindSurface } from "../lib/canvas-surface.js";
3
- import { bindFill } from "../lib/fill-watch.js";
4
3
 
5
4
  //#region src/elements/display-element.ts
6
5
  var SonicDisplayElement = class extends SonicElement {
@@ -38,17 +37,17 @@ var SonicDisplayElement = class extends SonicElement {
38
37
  #analyser;
39
38
  #buffer = /* @__PURE__ */ new Float32Array(0);
40
39
  #surface;
41
- #syncFill;
42
40
  attributeChangedCallback(name) {
43
- if (name === "fill") this.#syncFill?.(this.fill);
41
+ if (name === "fill") this.#surface?.setFill(this.fill);
44
42
  this.#surface?.invalidate();
45
43
  }
46
44
  connectedCallback() {
47
45
  this.upgradeProperties("analyser", "fill", "max", "min");
48
46
  super.connectedCallback();
49
47
  }
50
- push(frame) {
51
- this.receive(frame);
48
+ /** One level per bin, as `AnalyserNode.getFloatFrequencyData` fills it */
49
+ push(decibels) {
50
+ this.receive(decibels);
52
51
  this.#surface?.requestFrame();
53
52
  }
54
53
  repaint() {
@@ -60,6 +59,10 @@ var SonicDisplayElement = class extends SonicElement {
60
59
  this.#surface = bindSurface({
61
60
  canvas: this.canvas,
62
61
  colours: this.colours,
62
+ fill: {
63
+ control,
64
+ sizeProperty: this.sizeProperty
65
+ },
63
66
  numbers: this.numbers,
64
67
  paint: (context, frame) => {
65
68
  this.#paint(context, frame);
@@ -71,8 +74,7 @@ var SonicDisplayElement = class extends SonicElement {
71
74
  "pane",
72
75
  "well"
73
76
  ]);
74
- this.#syncFill = bindFill(control, this.sizeProperty, signal);
75
- this.#syncFill(this.fill);
77
+ this.#surface.setFill(this.fill);
76
78
  }
77
79
  surface() {
78
80
  return this.#surface;
@@ -1,13 +1,11 @@
1
1
  import { writeAttribute } from "../lib/write-attribute.js";
2
2
  import { SonicElement } from "./sonic-element.js";
3
3
  import { requireChild, template } from "../lib/render.js";
4
- import { bindDrag } from "../lib/pointer-drag.js";
5
4
  import { Readout } from "./readout.js";
6
- import { ReadoutClaim, revealDelay } from "./readout-claim.js";
7
- import { dragThresholdPx } from "../lib/drag-step.js";
5
+ import { ReadoutClaim } from "./readout-claim.js";
8
6
  import { SonicValueElement, linkValue } from "./value-element.js";
7
+ import { bindFieldGesture } from "./field-gesture.js";
9
8
  import { adsrPath, adsrShape, adsrStages, timeStages } from "../lib/adsr-shape.js";
10
- import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
11
9
 
12
10
  //#region src/elements/envelope.ts
13
11
  const curveStages = [
@@ -22,12 +20,6 @@ const bindable = [
22
20
  "release-curve"
23
21
  ];
24
22
  const bindableNames = new Set(bindable);
25
- function flip(point, isFlipped) {
26
- return isFlipped ? {
27
- ...point,
28
- y: -point.y
29
- } : point;
30
- }
31
23
  function isParent(node) {
32
24
  return "querySelector" in node;
33
25
  }
@@ -104,7 +96,7 @@ var SonicEnvelope = class extends SonicElement {
104
96
  this.reflect("hold", id);
105
97
  }
106
98
  get pointerType() {
107
- return this.#pointerType;
99
+ return this.#gesture?.pointerType();
108
100
  }
109
101
  get readout() {
110
102
  return this.hasAttribute("readout");
@@ -139,13 +131,12 @@ var SonicEnvelope = class extends SonicElement {
139
131
  #curveHandles = stageParts(this.#envelope, "sonic-envelope-curve", curveStages);
140
132
  #fill = requireChild(this.#envelope, ".sonic-envelope-fill", SVGElement);
141
133
  #floor = requireChild(this.#envelope, ".sonic-envelope-floor", SVGElement);
134
+ #gesture;
142
135
  #graph = requireChild(this.#envelope, ".sonic-envelope-graph", SVGElement);
143
136
  #handles = stageParts(this.#envelope, "sonic-envelope-handle", timeStages);
144
137
  #held;
145
138
  #isDriving = false;
146
139
  #line = requireChild(this.#envelope, ".sonic-envelope-line", SVGElement);
147
- #pointerDrag;
148
- #pointerType;
149
140
  #readout = new Readout(requireChild(this.#envelope, ".sonic-envelope-readout", HTMLDivElement));
150
141
  #shape = adsrShape({});
151
142
  #signal;
@@ -154,7 +145,7 @@ var SonicEnvelope = class extends SonicElement {
154
145
  attributeChangedCallback(name) {
155
146
  if (!this.#signal) return;
156
147
  if (name === "disabled") {
157
- this.#pointerDrag?.end();
148
+ this.#gesture?.end();
158
149
  this.#draw();
159
150
  return;
160
151
  }
@@ -183,13 +174,13 @@ var SonicEnvelope = class extends SonicElement {
183
174
  }, { once: true });
184
175
  }
185
176
  #bind() {
186
- this.#pointerDrag?.end();
177
+ this.#gesture?.end();
187
178
  this.#unbind();
188
179
  for (const name of bindable) {
189
180
  const element = this.#resolve(name);
190
181
  if (!element) continue;
191
182
  const link = linkValue(element);
192
- const unwatch = link.watch(() => {
183
+ const unwatch = link.model.watch(() => {
193
184
  if (!this.#isDriving) this.#draw();
194
185
  });
195
186
  this.#bindings.set(name, {
@@ -201,21 +192,23 @@ var SonicEnvelope = class extends SonicElement {
201
192
  this.#draw();
202
193
  }
203
194
  #bindPointer(envelope, signal) {
204
- this.#pointerDrag = bindDrag(envelope, {
205
- cancel: (drag) => {
206
- for (const { binding, from } of drag.drives) binding.link.input(from);
207
- },
195
+ this.#gesture = bindFieldGesture(envelope, {
196
+ claim: this.#claim,
208
197
  grab: (event) => this.#grab(event),
209
- move: (drag, event) => {
210
- this.#moveDrag(drag, event);
198
+ input: ({ drives }, next) => {
199
+ this.#isDriving = true;
200
+ for (const { axis, binding } of drives) {
201
+ const value = next[axis];
202
+ if (value !== void 0) binding.link.input(value);
203
+ }
204
+ this.#isDriving = false;
205
+ this.#draw();
211
206
  },
212
- release: (drag) => {
213
- this.#claim.press(false);
214
- for (const { binding, from } of drag.drives) {
215
- if (binding.link.value() === from) continue;
207
+ release: ({ drives }, moved) => {
208
+ for (const { axis, binding } of drives) {
209
+ if (!moved.includes(axis)) continue;
216
210
  binding.element.dispatchEvent(new Event("change", { bubbles: true }));
217
211
  }
218
- this.#pointerType = void 0;
219
212
  },
220
213
  toggle: (isDragging) => {
221
214
  this.toggleState("dragging", isDragging);
@@ -278,57 +271,23 @@ var SonicEnvelope = class extends SonicElement {
278
271
  #grab(event) {
279
272
  const pressed = this.#pressed(event);
280
273
  if (!pressed || pressed.drives.length === 0) return void 0;
281
- const { drives, isFlipped, part } = pressed;
282
- this.#pointerType = event.pointerType;
283
- this.#claim.press(true, revealDelay(this.#envelope));
284
- this.#held = part;
274
+ this.#held = pressed.part;
285
275
  this.#drawBracket();
286
276
  const box = this.#graph.getBoundingClientRect();
287
277
  const travel = {
288
278
  x: Math.max(1, box.width * this.#shape.share),
289
279
  y: Math.max(1, box.height)
290
280
  };
291
- const start = (axis) => {
292
- const link = drives.find((drive) => drive.axis === axis)?.binding.link;
293
- if (!link) return;
294
- const mapping = link.mapping();
295
- const from = link.value();
296
- return {
297
- from,
298
- mapping,
299
- proportion: mapping.proportionOf(from),
300
- travelPx: travel[axis]
301
- };
281
+ const axes = {};
282
+ for (const { axis, binding } of pressed.drives) axes[axis] = {
283
+ model: binding.link.model,
284
+ travelPx: travel[axis]
302
285
  };
303
286
  return {
304
- drives: drives.map((drive) => ({
305
- ...drive,
306
- from: drive.binding.link.value()
307
- })),
308
- isFlipped,
309
- part,
310
- state: startFieldDrag({
311
- position: flip(pointerPosition(event), isFlipped),
312
- thresholdPx: dragThresholdPx(event.pointerType),
313
- x: start("x"),
314
- y: start("y")
315
- })
287
+ ...pressed,
288
+ axes
316
289
  };
317
290
  }
318
- #moveDrag(drag, event) {
319
- const mappings = {};
320
- for (const { axis, binding } of drag.drives) mappings[axis] = binding.link.mapping();
321
- const step = stepFieldDrag(mappings, drag.state, flip(pointerMove(event), drag.isFlipped));
322
- drag.state = step.state;
323
- if (step.state.isEngaged) this.#claim.reveal("drag");
324
- this.#isDriving = true;
325
- for (const { axis, binding } of drag.drives) {
326
- const next = step[axis];
327
- if (next !== void 0) binding.link.input(next);
328
- }
329
- this.#isDriving = false;
330
- this.#draw();
331
- }
332
291
  #pressed(event) {
333
292
  const part = event.target instanceof Element ? event.target.closest(".sonic-envelope-handle, .sonic-envelope-curve") : void 0;
334
293
  const stage = part?.dataset.sonicStage;
@@ -344,10 +303,10 @@ var SonicEnvelope = class extends SonicElement {
344
303
  const proportions = {};
345
304
  const curves = {};
346
305
  for (const stage of adsrStages) {
347
- const link = this.#bindings.get(stage)?.link;
348
- proportions[stage] = link?.mapping().proportionOf(link.value());
306
+ const model = this.#bindings.get(stage)?.link.model;
307
+ proportions[stage] = model?.mapping().proportionOf(model.value);
349
308
  }
350
- for (const stage of curveStages) curves[stage] = this.#bindings.get(`${stage}-curve`)?.link.value();
309
+ for (const stage of curveStages) curves[stage] = this.#bindings.get(`${stage}-curve`)?.link.model.value;
351
310
  return adsrShape(proportions, curves);
352
311
  }
353
312
  #rebindOnce(key, arrives) {
@@ -359,7 +318,7 @@ var SonicEnvelope = class extends SonicElement {
359
318
  });
360
319
  }
361
320
  #renderReadout() {
362
- const drag = this.#pointerDrag?.current();
321
+ const drag = this.#gesture?.current();
363
322
  if (!drag) {
364
323
  this.#readout.close();
365
324
  return;
@@ -0,0 +1,81 @@
1
+ import { bindDrag } from "../lib/pointer-drag.js";
2
+ import { revealDelay } from "./readout-claim.js";
3
+ import { dragThresholdPx } from "../lib/drag-step.js";
4
+ import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
5
+
6
+ //#region src/elements/field-gesture.ts
7
+ const fieldAxes = ["x", "y"];
8
+ function flip(point, hold) {
9
+ return hold.isFlipped === true ? {
10
+ ...point,
11
+ y: -point.y
12
+ } : point;
13
+ }
14
+ function startAxis(hold) {
15
+ if (!hold) return;
16
+ const { model, travelPx } = hold;
17
+ const mapping = model.mapping();
18
+ const from = model.value;
19
+ return {
20
+ from,
21
+ mapping,
22
+ proportion: hold.proportion ?? mapping.proportionOf(from),
23
+ travelPx
24
+ };
25
+ }
26
+ function bindFieldGesture(target, options, signal) {
27
+ const { claim } = options;
28
+ let pointerType;
29
+ const drag = bindDrag(target, {
30
+ cancel: ({ from, hold }) => {
31
+ options.input(hold, from);
32
+ },
33
+ grab: (event) => {
34
+ pointerType = event.pointerType;
35
+ const hold = options.grab(event);
36
+ if (!hold) {
37
+ pointerType = void 0;
38
+ return;
39
+ }
40
+ const { x, y } = hold.axes;
41
+ claim.press(true, revealDelay(target));
42
+ return {
43
+ from: {
44
+ x: x?.from ?? x?.model.value,
45
+ y: y?.from ?? y?.model.value
46
+ },
47
+ hold,
48
+ state: startFieldDrag({
49
+ position: flip(pointerPosition(event), hold),
50
+ thresholdPx: dragThresholdPx(event.pointerType),
51
+ x: startAxis(x),
52
+ y: startAxis(y)
53
+ })
54
+ };
55
+ },
56
+ move: (held, event) => {
57
+ const { axes } = held.hold;
58
+ const step = stepFieldDrag({
59
+ x: axes.x?.model.mapping(),
60
+ y: axes.y?.model.mapping()
61
+ }, held.state, flip(pointerMove(event), held.hold));
62
+ held.state = step.state;
63
+ if (step.state.isEngaged) claim.reveal("drag");
64
+ options.input(held.hold, step);
65
+ },
66
+ release: ({ from, hold }) => {
67
+ claim.press(false);
68
+ options.release(hold, fieldAxes.filter((axis) => hold.axes[axis]?.model.value !== from[axis]));
69
+ pointerType = void 0;
70
+ },
71
+ toggle: options.toggle
72
+ }, signal);
73
+ return {
74
+ current: () => drag.current()?.hold,
75
+ end: drag.end,
76
+ pointerType: () => pointerType
77
+ };
78
+ }
79
+
80
+ //#endregion
81
+ export { bindFieldGesture };
@@ -49,6 +49,7 @@ var SonicMeter = class extends SonicElement {
49
49
  set ballistics(mode) {
50
50
  this.reflect("ballistics", mode);
51
51
  }
52
+ /** Linear peak amplitude, 1 at full scale; the meter converts to decibels and applies its ballistics */
52
53
  get level() {
53
54
  return this.#level;
54
55
  }
@@ -90,15 +91,18 @@ var SonicMeter = class extends SonicElement {
90
91
  set origin(value) {
91
92
  this.reflect("origin", value);
92
93
  }
94
+ /** Highest level since `resetPeak()`; `-Infinity` before any signal */
93
95
  get peak() {
94
96
  return this.#peak;
95
97
  }
98
+ /** Ladder thresholds, one per segment; unset draws a bar */
96
99
  get segments() {
97
100
  return parseNumberList(this.getAttribute("segments"));
98
101
  }
99
102
  set segments(thresholds) {
100
103
  this.reflect("segments", thresholds?.join(" "));
101
104
  }
105
+ /** A reading in the meter's own units, drawn as given with no ballistics; takes over from `level` while set */
102
106
  get value() {
103
107
  return this.optionalNumberAttribute("value");
104
108
  }