@xsynaptic/sonic-ui 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +112 -19
- package/dist/default/dat.js +1 -0
- package/dist/default/elements/button.js +2 -0
- package/dist/default/elements/dial.js +2 -0
- package/dist/default/elements/display-element.js +9 -7
- package/dist/default/elements/envelope.js +31 -72
- package/dist/default/elements/field-gesture.js +81 -0
- package/dist/default/elements/meter.js +4 -0
- package/dist/default/elements/position-group.js +42 -26
- package/dist/default/elements/radio-group.js +59 -56
- package/dist/default/elements/scale.js +3 -3
- package/dist/default/elements/slider.js +11 -13
- package/dist/default/elements/split.js +14 -13
- package/dist/default/elements/switch.js +31 -46
- package/dist/default/elements/toggle.js +20 -44
- package/dist/default/elements/value-element.js +65 -91
- package/dist/default/elements/value-gestures.js +22 -22
- package/dist/default/elements/wave-element.js +9 -12
- package/dist/default/elements/waveform.js +64 -249
- package/dist/default/elements/wavestrip.js +10 -5
- package/dist/default/elements/xy.js +67 -112
- package/dist/default/lib/canvas-surface.js +25 -0
- package/dist/default/lib/crossfade.js +1 -0
- package/dist/default/lib/envelope-curve.js +1 -0
- package/dist/default/lib/marker-rider.js +37 -14
- package/dist/default/lib/peaks-request.js +44 -0
- package/dist/default/lib/positions.js +58 -0
- package/dist/default/lib/scrub.js +12 -0
- package/dist/default/lib/value-model.js +62 -0
- package/dist/default/lib/waveform-scene.js +108 -0
- package/dist/default/lib/zoom-gesture.js +97 -1
- package/dist/dev/dat.d.ts +1 -0
- package/dist/dev/dat.js +1 -0
- package/dist/dev/dat.js.map +1 -1
- package/dist/dev/elements/button.d.ts +2 -0
- package/dist/dev/elements/button.js +2 -0
- package/dist/dev/elements/button.js.map +1 -1
- package/dist/dev/elements/dial.d.ts +2 -0
- package/dist/dev/elements/dial.js +2 -0
- package/dist/dev/elements/dial.js.map +1 -1
- package/dist/dev/elements/display-element.d.ts +5 -3
- package/dist/dev/elements/display-element.js +9 -7
- package/dist/dev/elements/display-element.js.map +1 -1
- package/dist/dev/elements/envelope.js +31 -72
- package/dist/dev/elements/envelope.js.map +1 -1
- package/dist/dev/elements/field-gesture.js +82 -0
- package/dist/dev/elements/field-gesture.js.map +1 -0
- package/dist/dev/elements/meter.d.ts +10 -5
- package/dist/dev/elements/meter.js +4 -0
- package/dist/dev/elements/meter.js.map +1 -1
- package/dist/dev/elements/position-group.d.ts +12 -6
- package/dist/dev/elements/position-group.js +42 -26
- package/dist/dev/elements/position-group.js.map +1 -1
- package/dist/dev/elements/radio-group.d.ts +11 -7
- package/dist/dev/elements/radio-group.js +59 -56
- package/dist/dev/elements/radio-group.js.map +1 -1
- package/dist/dev/elements/scale.js +3 -3
- package/dist/dev/elements/scale.js.map +1 -1
- package/dist/dev/elements/slider.d.ts +4 -2
- package/dist/dev/elements/slider.js +11 -13
- package/dist/dev/elements/slider.js.map +1 -1
- package/dist/dev/elements/split.d.ts +1 -0
- package/dist/dev/elements/split.js +14 -13
- package/dist/dev/elements/split.js.map +1 -1
- package/dist/dev/elements/switch.d.ts +2 -2
- package/dist/dev/elements/switch.js +31 -46
- package/dist/dev/elements/switch.js.map +1 -1
- package/dist/dev/elements/toggle.js +20 -44
- package/dist/dev/elements/toggle.js.map +1 -1
- package/dist/dev/elements/value-element.d.ts +5 -5
- package/dist/dev/elements/value-element.js +65 -91
- package/dist/dev/elements/value-element.js.map +1 -1
- package/dist/dev/elements/value-gestures.js +22 -22
- package/dist/dev/elements/value-gestures.js.map +1 -1
- package/dist/dev/elements/wave-element.d.ts +7 -4
- package/dist/dev/elements/wave-element.js +9 -12
- package/dist/dev/elements/wave-element.js.map +1 -1
- package/dist/dev/elements/waveform.d.ts +23 -22
- package/dist/dev/elements/waveform.js +64 -249
- package/dist/dev/elements/waveform.js.map +1 -1
- package/dist/dev/elements/wavestrip.d.ts +5 -1
- package/dist/dev/elements/wavestrip.js +10 -5
- package/dist/dev/elements/wavestrip.js.map +1 -1
- package/dist/dev/elements/xy.js +67 -112
- package/dist/dev/elements/xy.js.map +1 -1
- package/dist/dev/index.d.ts +3 -2
- package/dist/dev/lib/canvas-surface.d.ts +1 -0
- package/dist/dev/lib/canvas-surface.js +25 -0
- package/dist/dev/lib/canvas-surface.js.map +1 -1
- package/dist/dev/lib/crossfade.d.ts +1 -0
- package/dist/dev/lib/crossfade.js +1 -0
- package/dist/dev/lib/crossfade.js.map +1 -1
- package/dist/dev/lib/envelope-curve.d.ts +1 -0
- package/dist/dev/lib/envelope-curve.js +1 -0
- package/dist/dev/lib/envelope-curve.js.map +1 -1
- package/dist/dev/lib/marker-rider.js +37 -14
- package/dist/dev/lib/marker-rider.js.map +1 -1
- package/dist/dev/lib/peaks-request.d.ts +5 -0
- package/dist/dev/lib/peaks-request.js +45 -0
- package/dist/dev/lib/peaks-request.js.map +1 -0
- package/dist/dev/lib/positions.d.ts +7 -0
- package/dist/dev/lib/positions.js +59 -0
- package/dist/dev/lib/positions.js.map +1 -0
- package/dist/dev/lib/scrub.d.ts +8 -0
- package/dist/dev/lib/scrub.js +13 -0
- package/dist/dev/lib/scrub.js.map +1 -0
- package/dist/dev/lib/time-regions.d.ts +1 -0
- package/dist/dev/lib/time-regions.js.map +1 -1
- package/dist/dev/lib/units.d.ts +8 -0
- package/dist/dev/lib/value-model.js +63 -0
- package/dist/dev/lib/value-model.js.map +1 -0
- package/dist/dev/lib/waveform-buckets.d.ts +1 -0
- package/dist/dev/lib/waveform-buckets.js.map +1 -1
- package/dist/dev/lib/waveform-scene.d.ts +11 -0
- package/dist/dev/lib/waveform-scene.js +109 -0
- package/dist/dev/lib/waveform-scene.js.map +1 -0
- package/dist/dev/lib/zoom-gesture.js +97 -1
- package/dist/dev/lib/zoom-gesture.js.map +1 -1
- package/dist/styles/button.css +16 -6
- package/dist/styles/waveform.css +12 -4
- package/package.json +1 -1
- package/dist/default/lib/asked-value.js +0 -21
- package/dist/default/lib/fill-watch.js +0 -29
- package/dist/dev/lib/asked-value.js +0 -22
- package/dist/dev/lib/asked-value.js.map +0 -1
- package/dist/dev/lib/fill-watch.js +0 -30
- package/dist/dev/lib/fill-watch.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.17.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- - Waveform: `renderLabel(marker, element)` draws a marker's label in place of its text. A marker keeps a consumer's own keys for it to draw from.
|
|
21
|
+
- Waveform: `--sonic-waveform-label-fade-start` (0) and `--sonic-waveform-label-fade-end` (1) replace `--sonic-waveform-label-fade-ratio`. They place the parked label's fade between the next marker crossing the playhead, 0, and its label reaching the park, 1; equal values give no fade. By default the label now holds until the crossing and is gone as the next one lands.
|
|
22
|
+
|
|
3
23
|
## 0.16.0
|
|
4
24
|
|
|
5
25
|
### 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
|
-
<
|
|
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
|
|
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
|
-
|
|
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`.
|
|
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.
|
package/dist/default/dat.js
CHANGED
|
@@ -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.#
|
|
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
|
-
|
|
51
|
-
|
|
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.#
|
|
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
|
|
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.#
|
|
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.#
|
|
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.#
|
|
205
|
-
|
|
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
|
-
|
|
210
|
-
this.#
|
|
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: (
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
|
348
|
-
proportions[stage] =
|
|
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.#
|
|
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
|
}
|