Skip to content

Commit 9c0df38

Browse files
committed
UI: b/*.js: add simple user interface for track mute/solo/volume
Signed-off-by: Stefan Westerfeld <stefan@space.twc.de>
1 parent 0dc2e50 commit 9c0df38

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed

ui/b/trackview.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ b-trackview {
6161
.-track-name {
6262
display: inline-flex; position: relative; width: 7em; overflow: hidden;
6363
}
64+
.-mute-solo {
65+
display: flex;
66+
flex-direction: row;
67+
}
6468
}
6569
b-trackview[current-track] .b-trackview-control {
6670
background-color: zmod($b-button-border, Jz+=25%);
@@ -76,6 +80,11 @@ const HTML = (t, d) => html`
7680
selectall @change=${event => t.track.name (event.detail.value.trim())}
7781
>${t.wtrack_.name}</b-editable>
7882
</span>
83+
<span class="-mute-solo">
84+
<b-toggle @valuechange=${event => t.track.mute (event.target.value)} label="M"></b-toggle>
85+
<b-toggle @valuechange=${event => t.track.solo (event.target.value)} label="S"></b-toggle>
86+
<b-trackvolume .track="${t.track}" @valuechange=${event => t.track.volume (event.target.value)}></b-trackvolume>
87+
</span>
7988
<div class="-lvm-main">
8089
<div class="-lvm-levelbg" ${ref (h => t.levelbg_ = h)}></div>
8190
<div class="-lvm-covermid0" ${ref (h => t.covermid0_ = h)}></div>

ui/b/trackvolume.js

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
2+
// @ts-check
3+
4+
import { LitComponent, html, JsExtract, live, docs, ref } from '../little.js';
5+
import * as Util from '../util.js';
6+
7+
/** @class BTrackVolume
8+
* @description
9+
* The <b-trackvolume> element is an editor for that track volume
10+
* The input `value` will be constrained to take on an amount between `min` and `max` inclusively.
11+
* ### Properties:
12+
* *value*
13+
* : Contains the number being edited.
14+
* *track*
15+
* : The track
16+
* ### Events:
17+
* *valuechange*
18+
* : Event emitted whenever the volume changes.
19+
*/
20+
21+
// <STYLE/>
22+
JsExtract.scss`
23+
b-trackvolume {
24+
display: flex; justify-content: flex-end;
25+
.-trackvolume-bg {
26+
background-color: rgba( 0, 0, 0, .80);
27+
width: 10em;
28+
height: 1em;
29+
margin: 2px;
30+
}
31+
.-trackvolume-fg {
32+
background-color: #999999;
33+
height: 1em;
34+
}
35+
}`;
36+
37+
// <HTML/>
38+
const HTML = t => [
39+
html`
40+
<div class="-trackvolume-bg" @pointerdown="${t.pointerdown}">
41+
<div class="-trackvolume-fg" style="width: ${t.percent}%">
42+
</div>
43+
</div>
44+
`
45+
];
46+
47+
const OBJ_ATTRIBUTE = { type: Object, reflect: true }; // sync attribute with property
48+
49+
// <SCRIPT/>
50+
class BTrackVolume extends LitComponent {
51+
createRenderRoot() { return this; }
52+
render() { return HTML (this); }
53+
static properties = {
54+
track: OBJ_ATTRIBUTE,
55+
value: { type: Number },
56+
percent: { type: Number },
57+
};
58+
constructor() {
59+
super();
60+
this.value = 0;
61+
this.percent = 0;
62+
this.last_ = 0;
63+
this.track = null;
64+
}
65+
updated (changed_props)
66+
{
67+
this.update_value();
68+
}
69+
async update_value()
70+
{
71+
this.value = await this.track.volume();
72+
this.percent = this.value * 100;
73+
this.last_ = this.value;
74+
}
75+
pointerdown (event)
76+
{
77+
spin_drag_start (this, event, this.drag_change.bind (this));
78+
}
79+
drag_change (distance)
80+
{
81+
this.last_ = Util.clamp (this.last_ + distance, 0, +1);
82+
this.track.volume (this.last_);
83+
this.value = this.last_;
84+
this.percent = this.value * 100;
85+
}
86+
}
87+
customElements.define ('b-trackvolume', BTrackVolume);
88+
89+
const SPIN_DRAG = Symbol ('SpinDrag');
90+
const USE_PTRLOCK = true;
91+
92+
/// Setup drag handlers for numeric spin button behavior.
93+
export function spin_drag_start (element, event, value_callback)
94+
{
95+
console.assert (element instanceof Element);
96+
console.assert (event.type === 'pointerdown');
97+
// allow only primary button press (single click)
98+
if (event.buttons != 1 || element[SPIN_DRAG])
99+
{
100+
if (element[SPIN_DRAG])
101+
spin_drag_stop (element);
102+
return false;
103+
}
104+
const spin_drag = {};
105+
Object.assign (spin_drag, {
106+
element,
107+
value_callback,
108+
pending_change: null,
109+
captureid: undefined,
110+
unlock_pointer: undefined,
111+
stop_event: event => { event.preventDefault(); event.stopPropagation(); },
112+
pointermove: spin_drag_pointermove.bind (spin_drag),
113+
stop: spin_drag_stop.bind (spin_drag),
114+
ptraccel: 1.0,
115+
last: null,
116+
drag: null,
117+
});
118+
// setup drag mode
119+
try {
120+
spin_drag.element.setPointerCapture (event.pointerId);
121+
spin_drag.captureid = event.pointerId;
122+
} catch (e) {
123+
// something went wrong, bail out the drag
124+
console.warn ('drag_start: error:', /**@type{Error}*/ (e).message);
125+
return false;
126+
}
127+
// use pointer lock for knob turning
128+
if (USE_PTRLOCK)
129+
spin_drag.unlock_pointer = Util.request_pointer_lock (element);
130+
const has_ptrlock = Util.has_pointer_lock (element);
131+
spin_drag.last = { x: event.pageX, y: event.pageY };
132+
spin_drag.drag = has_ptrlock ? { x: 0, y: 0 } : { x: event.pageX, y: event.pageY };
133+
spin_drag.stop_event (event);
134+
document.body.addEventListener ('wheel', spin_drag.stop_event, { capture: true, passive: false });
135+
element.addEventListener ('pointermove', spin_drag.pointermove);
136+
element.addEventListener ('pointerup', spin_drag.stop);
137+
element[SPIN_DRAG] = spin_drag;
138+
Shell.data_bubble.force (element);
139+
return true; // spin drag started
140+
}
141+
142+
/** Stop sping drag event handlers and pointer grab.
143+
* @this{any}
144+
*/
145+
function spin_drag_stop (event_or_element= undefined)
146+
{
147+
const spin_drag = event_or_element instanceof MouseEvent ? this : event_or_element[SPIN_DRAG];
148+
if (!spin_drag?.stop)
149+
return;
150+
const element = spin_drag.element;
151+
if (event_or_element instanceof MouseEvent)
152+
spin_drag.stop_event (event_or_element);
153+
element.removeEventListener ('pointerup', spin_drag.stop);
154+
element.removeEventListener ('pointermove', spin_drag.pointermove);
155+
document.body.removeEventListener ('wheel', spin_drag.stop_event, { capture: true, /*passive: false*/ });
156+
// unset drag mode
157+
spin_drag.unlock_pointer = spin_drag.unlock_pointer?.();
158+
if (spin_drag.captureid !== undefined)
159+
element.releasePointerCapture (spin_drag.captureid);
160+
spin_drag.captureid = undefined;
161+
spin_drag.pending_change = cancelAnimationFrame (spin_drag.pending_change);
162+
spin_drag.last = null;
163+
spin_drag.drag = null;
164+
spin_drag.pointermove = null;
165+
spin_drag.stop = null;
166+
delete element[SPIN_DRAG];
167+
Shell.data_bubble.unforce (element);
168+
}
169+
170+
/** Handle sping drag pointer motion.
171+
* @this{any}
172+
*/
173+
function spin_drag_pointermove (event)
174+
{
175+
console.assert (event.type === 'pointermove');
176+
const spin_drag = this, element = spin_drag.element;
177+
if (!spin_drag.pending_change) // debounce value updates
178+
spin_drag.pending_change = requestAnimationFrame (() => {
179+
spin_drag.pending_change = null;
180+
spin_drag_change.call (spin_drag);
181+
});
182+
const has_ptrlock = Util.has_pointer_lock (element);
183+
if (has_ptrlock)
184+
{
185+
spin_drag.drag.x += event.movementX;
186+
spin_drag.drag.y += event.movementY;
187+
}
188+
else
189+
spin_drag.drag = { x: event.pageX, y: event.pageY };
190+
spin_drag.ptraccel = spin_drag_granularity (event);
191+
spin_drag.stop_event (event);
192+
}
193+
194+
/** Turn accumulated spin drag motions into actual value changes.
195+
* @this{any}
196+
*/
197+
function spin_drag_change ()
198+
{
199+
const spin_drag = this, element = spin_drag.element;
200+
const drag = spin_drag.drag, last = spin_drag.last;
201+
const has_ptrlock = Util.has_pointer_lock (element);
202+
const dx = (has_ptrlock ? drag.x : drag.x - last.x) * 0.5;
203+
const dy = has_ptrlock ? drag.y : drag.y - last.y;
204+
let s = true; // adjust via Increase and:
205+
if (dy > 0) // if DOWN
206+
s = dx >= dy; // Decrease unless mostly RIGHT
207+
else if (dx < 0) // if LEFT
208+
s = dy <= dx; // Decrease unless mostly UP
209+
// reset drag accumulator
210+
if (has_ptrlock)
211+
spin_drag.drag = { x: 0, y: 0 };
212+
else
213+
spin_drag.last = { x: drag.x, y: drag.y };
214+
// determine accumulated distance
215+
let dist = (s ? +1 : -1) * Math.sqrt (dx * dx + dy * dy) * spin_drag.ptraccel;
216+
// convert to physical pixel movements, so knob behaviour is unrelated to display resolution
217+
if (!has_ptrlock ||
218+
(has_ptrlock && CONFIG.dpr_movement))
219+
{
220+
const DPR = window.devicePixelRatio || 1;
221+
dist *= DPR;
222+
}
223+
// assign value, stop dragging if return is true
224+
if (spin_drag.value_callback (dist))
225+
spin_drag_stop (element);
226+
}
227+
228+
/// Calculate spin drag acceleration (slowdown) from event type and modifiers.
229+
function spin_drag_granularity (event)
230+
{
231+
if (event.type == 'wheel') {
232+
let gran = 0.025; // approximate wheel delta "step" to percent
233+
if (event.shiftKey)
234+
gran = 0.005; // slow down
235+
else if (event.ctrlKey)
236+
gran = 0.10; // speed up
237+
return gran;
238+
}
239+
// pixel drag
240+
const radius = 64; // assumed knob size
241+
const circum = 2 * radius * Math.PI;
242+
let gran = 1 / circum; // steps per full turn to feel natural
243+
if (event.shiftKey)
244+
gran = gran * 0.1; // slow down
245+
else if (event.ctrlKey)
246+
gran = gran * 10; // speed up
247+
return gran;
248+
}

0 commit comments

Comments
 (0)