@tremolo-ui/dom 0.5.0 → 0.6.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/dist/index.cjs +459 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +234 -39
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +234 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +457 -102
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/canvas/animation.ts +49 -31
- package/src/canvas/context.ts +32 -5
- package/src/index.ts +13 -2
- package/src/midi/access.ts +83 -13
- package/src/midi/input.ts +97 -26
- package/src/midi/message.ts +37 -8
- package/src/piano/{input.ts → index.ts} +23 -3
- package/src/pointer/{dragValue.ts → drag-value.ts} +155 -19
- package/src/pointer/drag.ts +215 -20
- package/src/pointer/wheel.ts +4 -2
- package/src/selection/box.ts +136 -0
package/dist/index.cjs
CHANGED
|
@@ -2,9 +2,10 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _tremolo_ui_functions = require("@tremolo-ui/functions");
|
|
3
3
|
//#region src/canvas/context.ts
|
|
4
4
|
/**
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The assignable parts of a 2D context's drawing state. They have to be
|
|
6
|
+
* carried across a resize by hand because setting `canvas.width` resets the
|
|
7
|
+
* context to its defaults. The transform and line dash are handled separately
|
|
8
|
+
* by {@link DrawingContext} because they are exposed through methods.
|
|
8
9
|
*
|
|
9
10
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save
|
|
10
11
|
*/
|
|
@@ -22,24 +23,37 @@ const drawingState = [
|
|
|
22
23
|
"shadowBlur",
|
|
23
24
|
"shadowColor",
|
|
24
25
|
"globalCompositeOperation",
|
|
26
|
+
"filter",
|
|
25
27
|
"font",
|
|
28
|
+
"fontKerning",
|
|
29
|
+
"fontStretch",
|
|
30
|
+
"fontVariantCaps",
|
|
26
31
|
"textAlign",
|
|
27
32
|
"textBaseline",
|
|
28
33
|
"direction",
|
|
29
|
-
"
|
|
34
|
+
"letterSpacing",
|
|
35
|
+
"textRendering",
|
|
36
|
+
"wordSpacing",
|
|
37
|
+
"imageSmoothingEnabled",
|
|
38
|
+
"imageSmoothingQuality"
|
|
30
39
|
];
|
|
31
40
|
function isDrawingState(value) {
|
|
32
|
-
return typeof value
|
|
41
|
+
return typeof value === "string" && drawingState.includes(value);
|
|
33
42
|
}
|
|
34
43
|
/** Copy the drawing state off a context, to put back after a resize. */
|
|
35
44
|
function readDrawingState(context) {
|
|
36
45
|
const state = {};
|
|
37
46
|
for (const property of drawingState) state[property] = context[property];
|
|
47
|
+
state.lineDash = context.getLineDash();
|
|
48
|
+
state.transform = context.getTransform();
|
|
38
49
|
return state;
|
|
39
50
|
}
|
|
40
51
|
/** Put a state read by {@link readDrawingState} back onto a context. */
|
|
41
|
-
function writeDrawingState(context, state) {
|
|
52
|
+
function writeDrawingState(context, state, transformScale = 1) {
|
|
42
53
|
for (const property of drawingState) context[property] = state[property];
|
|
54
|
+
context.setLineDash(state.lineDash);
|
|
55
|
+
const { a, b, c, d, e, f } = state.transform;
|
|
56
|
+
context.setTransform(a * transformScale, b * transformScale, c * transformScale, d * transformScale, e * transformScale, f * transformScale);
|
|
43
57
|
}
|
|
44
58
|
/**
|
|
45
59
|
* Give the canvas a backing store of `width * dpr` by `height * dpr` device
|
|
@@ -72,12 +86,13 @@ function createAnimationCanvas(canvas, options) {
|
|
|
72
86
|
const context2d = canvas.getContext("2d", opts.contextAttributes);
|
|
73
87
|
if (!context2d) throw new Error("createAnimationCanvas: cannot get a 2d context");
|
|
74
88
|
const context = context2d;
|
|
75
|
-
const
|
|
89
|
+
const resizable = opts.resizable ?? false;
|
|
76
90
|
/** Off-document canvas holding the drawing while the real one is resized. */
|
|
77
91
|
let memo = null;
|
|
78
92
|
let memoContext = null;
|
|
79
93
|
let width = 0;
|
|
80
94
|
let height = 0;
|
|
95
|
+
let appliedDpr = 0;
|
|
81
96
|
/** Whether a size has been applied, so a frame can be drawn. */
|
|
82
97
|
let sized = false;
|
|
83
98
|
let initialized = false;
|
|
@@ -95,17 +110,16 @@ function createAnimationCanvas(canvas, options) {
|
|
|
95
110
|
* the copy neither scales nor resamples.
|
|
96
111
|
*/
|
|
97
112
|
function takeSnapshot() {
|
|
98
|
-
if (!(opts.reduceFlickering ?? true)) return
|
|
113
|
+
if (!(opts.reduceFlickering ?? true)) return false;
|
|
99
114
|
if (!memo) {
|
|
100
115
|
memo = globalThis.document?.createElement("canvas") ?? null;
|
|
101
116
|
memoContext = memo?.getContext("2d", opts.contextAttributes) ?? null;
|
|
102
117
|
}
|
|
103
|
-
if (!memo || !memoContext) return
|
|
104
|
-
const state = readDrawingState(context);
|
|
118
|
+
if (!memo || !memoContext) return false;
|
|
105
119
|
memo.width = canvas.width;
|
|
106
120
|
memo.height = canvas.height;
|
|
107
121
|
if (canvas.width > 0 && canvas.height > 0) memoContext.drawImage(canvas, 0, 0);
|
|
108
|
-
return
|
|
122
|
+
return true;
|
|
109
123
|
}
|
|
110
124
|
/**
|
|
111
125
|
* Put the snapshot back, given the size in CSS pixels it was taken at.
|
|
@@ -115,25 +129,32 @@ function createAnimationCanvas(canvas, options) {
|
|
|
115
129
|
* stays put that is a 1:1 copy of device pixels, and when it changes the
|
|
116
130
|
* snapshot is rescaled once, from its full resolution.
|
|
117
131
|
*/
|
|
118
|
-
function restoreSnapshot(
|
|
119
|
-
if (!
|
|
132
|
+
function restoreSnapshot(hasSnapshot, previousWidth, previousHeight) {
|
|
133
|
+
if (!hasSnapshot || !memo) return;
|
|
120
134
|
if (memo.width <= 0 || memo.height <= 0) return;
|
|
121
|
-
writeDrawingState(context, state);
|
|
122
135
|
context.drawImage(memo, 0, 0, previousWidth, previousHeight);
|
|
123
136
|
}
|
|
124
137
|
function applySize(w, h) {
|
|
125
138
|
const dpr = devicePixelRatio();
|
|
126
139
|
const previousWidth = width;
|
|
127
140
|
const previousHeight = height;
|
|
128
|
-
const
|
|
141
|
+
const previousDpr = appliedDpr;
|
|
142
|
+
const state = sized ? readDrawingState(context) : null;
|
|
143
|
+
const hasSnapshot = sized && takeSnapshot();
|
|
129
144
|
applyDevicePixelRatio(canvas, context, w, h, dpr);
|
|
130
145
|
width = w;
|
|
131
146
|
height = h;
|
|
147
|
+
appliedDpr = dpr;
|
|
132
148
|
sized = true;
|
|
133
|
-
restoreSnapshot(
|
|
149
|
+
restoreSnapshot(hasSnapshot, previousWidth, previousHeight);
|
|
150
|
+
if (state) writeDrawingState(context, state, dpr / previousDpr);
|
|
134
151
|
}
|
|
135
|
-
function
|
|
152
|
+
function applyDevicePixelRatioIfNeeded() {
|
|
153
|
+
if (sized && devicePixelRatio() !== appliedDpr) applySize(width, height);
|
|
154
|
+
}
|
|
155
|
+
function drawFrame(timestamp = performance.now()) {
|
|
136
156
|
if (!sized) return;
|
|
157
|
+
applyDevicePixelRatioIfNeeded();
|
|
137
158
|
if (!initialized) {
|
|
138
159
|
initialized = true;
|
|
139
160
|
opts.init?.(context, {
|
|
@@ -141,41 +162,40 @@ function createAnimationCanvas(canvas, options) {
|
|
|
141
162
|
height
|
|
142
163
|
});
|
|
143
164
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
previousTime = now;
|
|
165
|
+
const deltaTime = timestamp - previousTime;
|
|
166
|
+
previousTime = timestamp;
|
|
147
167
|
count += 1;
|
|
148
168
|
opts.draw(context, {
|
|
149
169
|
width,
|
|
150
170
|
height,
|
|
151
171
|
count,
|
|
152
172
|
deltaTime,
|
|
153
|
-
elapsedTime:
|
|
154
|
-
fps: 1e3 / deltaTime
|
|
173
|
+
elapsedTime: timestamp - startTime,
|
|
174
|
+
fps: deltaTime > 0 ? 1e3 / deltaTime : 0
|
|
155
175
|
});
|
|
156
176
|
}
|
|
157
|
-
function tick() {
|
|
177
|
+
function tick(timestamp) {
|
|
158
178
|
frameId = requestAnimationFrame(tick);
|
|
159
|
-
drawFrame();
|
|
179
|
+
drawFrame(timestamp);
|
|
160
180
|
}
|
|
161
181
|
function startLoop() {
|
|
162
|
-
if (frameId
|
|
182
|
+
if (frameId !== null) return;
|
|
163
183
|
previousTime = performance.now();
|
|
164
184
|
frameId = requestAnimationFrame(tick);
|
|
165
185
|
}
|
|
166
186
|
function stopLoop() {
|
|
167
|
-
if (frameId
|
|
187
|
+
if (frameId === null) return;
|
|
168
188
|
cancelAnimationFrame(frameId);
|
|
169
189
|
frameId = null;
|
|
170
190
|
}
|
|
171
191
|
let observer = null;
|
|
172
|
-
if (
|
|
192
|
+
if (resizable) {
|
|
173
193
|
const parent = canvas.parentElement;
|
|
174
|
-
if (!parent) throw new Error("createAnimationCanvas:
|
|
194
|
+
if (!parent) throw new Error("createAnimationCanvas: resizable needs the canvas to have a parent element");
|
|
175
195
|
observer = new ResizeObserver((entries) => {
|
|
176
196
|
for (const entry of entries) {
|
|
177
197
|
const { width: w, height: h } = entry.contentRect;
|
|
178
|
-
applySize(w, h);
|
|
198
|
+
if (w !== width || h !== height || devicePixelRatio() !== appliedDpr) applySize(w, h);
|
|
179
199
|
if (!(opts.animate ?? true)) drawFrame();
|
|
180
200
|
}
|
|
181
201
|
});
|
|
@@ -192,13 +212,14 @@ function createAnimationCanvas(canvas, options) {
|
|
|
192
212
|
opts = {
|
|
193
213
|
...opts,
|
|
194
214
|
...next,
|
|
195
|
-
|
|
215
|
+
resizable,
|
|
196
216
|
contextAttributes: opts.contextAttributes
|
|
197
217
|
};
|
|
198
|
-
if (!
|
|
218
|
+
if (!resizable) {
|
|
199
219
|
const { width: w = 100, height: h = 100 } = opts.size ?? {};
|
|
200
|
-
if (w
|
|
220
|
+
if (w !== width || h !== height) applySize(w, h);
|
|
201
221
|
}
|
|
222
|
+
applyDevicePixelRatioIfNeeded();
|
|
202
223
|
const animating = opts.animate ?? true;
|
|
203
224
|
if (animating && !wasAnimating) startLoop();
|
|
204
225
|
if (!animating && wasAnimating) stopLoop();
|
|
@@ -220,11 +241,26 @@ function createAnimationCanvas(canvas, options) {
|
|
|
220
241
|
const PERMISSION_DENIED = "PERMISSION_DENIED";
|
|
221
242
|
/** @private */
|
|
222
243
|
const NOT_SUPPORTED = "NOT_SUPPORTED";
|
|
244
|
+
/** @private */
|
|
245
|
+
const UNAVAILABLE = "UNAVAILABLE";
|
|
223
246
|
const INITIAL_STATE = {
|
|
224
247
|
midiAccess: null,
|
|
225
|
-
error: null
|
|
248
|
+
error: null,
|
|
249
|
+
inputs: []
|
|
226
250
|
};
|
|
227
251
|
/**
|
|
252
|
+
* Which of our errors a rejected `requestMIDIAccess()` amounts to.
|
|
253
|
+
*
|
|
254
|
+
* The spec names the reasons, and they need telling apart: a user who said no
|
|
255
|
+
* can say yes on a second ask, while a browser without the API never will.
|
|
256
|
+
*/
|
|
257
|
+
function toError(reason) {
|
|
258
|
+
const name = typeof reason === "object" && reason !== null && "name" in reason ? String(reason.name) : "";
|
|
259
|
+
if (name === "SecurityError" || name === "NotAllowedError") return PERMISSION_DENIED;
|
|
260
|
+
if (name === "NotSupportedError" || name === "TypeError") return NOT_SUPPORTED;
|
|
261
|
+
return UNAVAILABLE;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
228
264
|
* Request MIDI access in the browser.
|
|
229
265
|
*
|
|
230
266
|
* The returned instance holds the state and notifies subscribers when it changes,
|
|
@@ -233,13 +269,31 @@ const INITIAL_STATE = {
|
|
|
233
269
|
function createMIDIAccess() {
|
|
234
270
|
let state = INITIAL_STATE;
|
|
235
271
|
let destroyed = false;
|
|
272
|
+
let access = null;
|
|
273
|
+
let requestGeneration = 0;
|
|
236
274
|
const listeners = /* @__PURE__ */ new Set();
|
|
237
275
|
function setState(next) {
|
|
238
276
|
state = next;
|
|
239
277
|
for (const listener of listeners) listener();
|
|
240
278
|
}
|
|
241
|
-
|
|
279
|
+
/**
|
|
280
|
+
* A new array every time, so that `useSyncExternalStore` and its equivalents
|
|
281
|
+
* see the change. The identity of the state object is what they compare.
|
|
282
|
+
*/
|
|
283
|
+
function readInputs() {
|
|
284
|
+
return access ? [...access.inputs.values()] : [];
|
|
285
|
+
}
|
|
286
|
+
function handleStateChange(event) {
|
|
287
|
+
if (destroyed || !access) return;
|
|
288
|
+
if (event && event.port?.type === "output") return;
|
|
289
|
+
setState({
|
|
290
|
+
...state,
|
|
291
|
+
inputs: readInputs()
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function request(options = {}) {
|
|
242
295
|
if (destroyed) return;
|
|
296
|
+
const generation = ++requestGeneration;
|
|
243
297
|
if (typeof navigator === "undefined" || !navigator.requestMIDIAccess) {
|
|
244
298
|
setState({
|
|
245
299
|
...state,
|
|
@@ -247,17 +301,21 @@ function createMIDIAccess() {
|
|
|
247
301
|
});
|
|
248
302
|
return;
|
|
249
303
|
}
|
|
250
|
-
navigator.requestMIDIAccess().then((
|
|
251
|
-
if (destroyed) return;
|
|
304
|
+
navigator.requestMIDIAccess({ sysex: options.sysex ?? false }).then((granted) => {
|
|
305
|
+
if (destroyed || generation !== requestGeneration) return;
|
|
306
|
+
access?.removeEventListener("statechange", handleStateChange);
|
|
307
|
+
access = granted;
|
|
308
|
+
granted.addEventListener("statechange", handleStateChange);
|
|
252
309
|
setState({
|
|
253
|
-
midiAccess:
|
|
254
|
-
error: null
|
|
310
|
+
midiAccess: granted,
|
|
311
|
+
error: null,
|
|
312
|
+
inputs: readInputs()
|
|
255
313
|
});
|
|
256
|
-
}
|
|
257
|
-
if (destroyed) return;
|
|
314
|
+
}, (reason) => {
|
|
315
|
+
if (destroyed || generation !== requestGeneration) return;
|
|
258
316
|
setState({
|
|
259
317
|
...state,
|
|
260
|
-
error:
|
|
318
|
+
error: toError(reason)
|
|
261
319
|
});
|
|
262
320
|
});
|
|
263
321
|
}
|
|
@@ -273,6 +331,9 @@ function createMIDIAccess() {
|
|
|
273
331
|
},
|
|
274
332
|
destroy: () => {
|
|
275
333
|
destroyed = true;
|
|
334
|
+
requestGeneration += 1;
|
|
335
|
+
access?.removeEventListener("statechange", handleStateChange);
|
|
336
|
+
access = null;
|
|
276
337
|
listeners.clear();
|
|
277
338
|
}
|
|
278
339
|
};
|
|
@@ -282,35 +343,110 @@ function createMIDIAccess() {
|
|
|
282
343
|
/**
|
|
283
344
|
* Listen to raw `midimessage` events on every input of a MIDIAccess.
|
|
284
345
|
*
|
|
346
|
+
* The set of inputs is followed rather than sampled: MIDIAccess fires
|
|
347
|
+
* `statechange` when a device is plugged in or unplugged, and the listeners
|
|
348
|
+
* move with it. A keyboard connected after access was granted works without
|
|
349
|
+
* the caller having to rebuild anything.
|
|
350
|
+
*
|
|
285
351
|
* Use this when you need more detail than {@link createMIDIInput} provides.
|
|
286
352
|
*/
|
|
287
353
|
function createMIDIMessage(midiAccess, onMIDIMessage) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
354
|
+
let handler = onMIDIMessage;
|
|
355
|
+
/** Inputs this instance currently listens on. */
|
|
356
|
+
const attached = /* @__PURE__ */ new Set();
|
|
357
|
+
const listener = (event) => handler(event);
|
|
358
|
+
function sync() {
|
|
359
|
+
if (!midiAccess) return;
|
|
360
|
+
const connected = new Set(midiAccess.inputs.values());
|
|
361
|
+
for (const input of connected) {
|
|
362
|
+
if (attached.has(input)) continue;
|
|
363
|
+
input.addEventListener("midimessage", listener);
|
|
364
|
+
attached.add(input);
|
|
365
|
+
}
|
|
366
|
+
for (const input of [...attached]) {
|
|
367
|
+
if (connected.has(input)) continue;
|
|
368
|
+
input.removeEventListener("midimessage", listener);
|
|
369
|
+
attached.delete(input);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
sync();
|
|
373
|
+
midiAccess?.addEventListener("statechange", sync);
|
|
374
|
+
return {
|
|
375
|
+
update: (next) => {
|
|
376
|
+
handler = next;
|
|
377
|
+
},
|
|
378
|
+
destroy: () => {
|
|
379
|
+
midiAccess?.removeEventListener("statechange", sync);
|
|
380
|
+
for (const input of attached) input.removeEventListener("midimessage", listener);
|
|
381
|
+
attached.clear();
|
|
382
|
+
}
|
|
383
|
+
};
|
|
294
384
|
}
|
|
295
385
|
//#endregion
|
|
296
386
|
//#region src/midi/input.ts
|
|
297
|
-
|
|
298
|
-
|
|
387
|
+
/** Status byte of each channel message, with the channel nibble cleared. */
|
|
388
|
+
const STATUS = {
|
|
299
389
|
NOTE_OFF: 128,
|
|
390
|
+
NOTE_ON: 144,
|
|
391
|
+
AFTERTOUCH: 160,
|
|
392
|
+
CONTROL_CHANGE: 176,
|
|
393
|
+
PROGRAM_CHANGE: 192,
|
|
394
|
+
CHANNEL_PRESSURE: 208,
|
|
300
395
|
PITCH_BEND: 224
|
|
301
396
|
};
|
|
302
397
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
398
|
+
* Centre of the 14-bit pitch bend range: no bend.
|
|
399
|
+
*
|
|
400
|
+
* The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above
|
|
401
|
+
* — so a wheel at rest reports exactly this rather than half of the maximum.
|
|
305
402
|
*/
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
403
|
+
const PITCH_BEND_CENTER = 8192;
|
|
404
|
+
/**
|
|
405
|
+
* Handle the channel voice messages of every connected input. To be used with
|
|
406
|
+
* {@link createMIDIAccess}. Internally uses {@link createMIDIMessage}, so
|
|
407
|
+
* devices plugged in later are picked up.
|
|
408
|
+
*
|
|
409
|
+
* System messages (clock, sysex, and the rest of `0xf0`-`0xff`) are not
|
|
410
|
+
* decoded here; reach for {@link createMIDIMessage} for those.
|
|
411
|
+
*/
|
|
412
|
+
function createMIDIInput(midiAccess, handlers) {
|
|
413
|
+
let current = handlers;
|
|
414
|
+
return {
|
|
415
|
+
update: (next) => {
|
|
416
|
+
current = next;
|
|
417
|
+
},
|
|
418
|
+
destroy: createMIDIMessage(midiAccess, (event) => {
|
|
419
|
+
if (!event.data || event.data.length === 0) return;
|
|
420
|
+
const status = event.data[0];
|
|
421
|
+
if (status >= 240) return;
|
|
422
|
+
const kind = status & 240;
|
|
423
|
+
const channel = status & 15;
|
|
424
|
+
const first = event.data[1] ?? 0;
|
|
425
|
+
const second = event.data[2] ?? 0;
|
|
426
|
+
switch (kind) {
|
|
427
|
+
case STATUS.NOTE_OFF:
|
|
428
|
+
current.onNoteOffEvent?.(first, channel);
|
|
429
|
+
break;
|
|
430
|
+
case STATUS.NOTE_ON:
|
|
431
|
+
if (second === 0) current.onNoteOffEvent?.(first, channel);
|
|
432
|
+
else current.onNoteOnEvent?.(first, second, channel);
|
|
433
|
+
break;
|
|
434
|
+
case STATUS.AFTERTOUCH:
|
|
435
|
+
current.onAftertouchEvent?.(first, second, channel);
|
|
436
|
+
break;
|
|
437
|
+
case STATUS.CONTROL_CHANGE:
|
|
438
|
+
current.onControlChangeEvent?.(first, second, channel);
|
|
439
|
+
break;
|
|
440
|
+
case STATUS.PROGRAM_CHANGE:
|
|
441
|
+
current.onProgramChangeEvent?.(first, channel);
|
|
442
|
+
break;
|
|
443
|
+
case STATUS.CHANNEL_PRESSURE:
|
|
444
|
+
current.onChannelPressureEvent?.(first, channel);
|
|
445
|
+
break;
|
|
446
|
+
case STATUS.PITCH_BEND: current.onPitchBendEvent?.(second << 7 | first, channel);
|
|
447
|
+
}
|
|
448
|
+
}).destroy
|
|
449
|
+
};
|
|
314
450
|
}
|
|
315
451
|
//#endregion
|
|
316
452
|
//#region src/pointer/drag.ts
|
|
@@ -327,6 +463,7 @@ const MANAGED_STYLES = [
|
|
|
327
463
|
["-webkit-user-select", "none"],
|
|
328
464
|
["-webkit-touch-callout", "none"]
|
|
329
465
|
];
|
|
466
|
+
const managedStyleStates = /* @__PURE__ */ new WeakMap();
|
|
330
467
|
/**
|
|
331
468
|
* Track a pointer drag on an element.
|
|
332
469
|
*
|
|
@@ -343,10 +480,24 @@ function createDrag(element, options = {}) {
|
|
|
343
480
|
const multiPointer = options.multiPointer ?? false;
|
|
344
481
|
const capture = element;
|
|
345
482
|
const style = element.style;
|
|
346
|
-
|
|
347
|
-
if (style)
|
|
348
|
-
|
|
349
|
-
|
|
483
|
+
let managedStyles = managedStyleStates.get(element);
|
|
484
|
+
if (style) {
|
|
485
|
+
if (managedStyles) managedStyles.count += 1;
|
|
486
|
+
else {
|
|
487
|
+
const previous = /* @__PURE__ */ new Map();
|
|
488
|
+
for (const [property, value] of MANAGED_STYLES) {
|
|
489
|
+
previous.set(property, {
|
|
490
|
+
value: style.getPropertyValue(property),
|
|
491
|
+
priority: style.getPropertyPriority(property)
|
|
492
|
+
});
|
|
493
|
+
style.setProperty(property, value);
|
|
494
|
+
}
|
|
495
|
+
managedStyles = {
|
|
496
|
+
count: 1,
|
|
497
|
+
previous
|
|
498
|
+
};
|
|
499
|
+
managedStyleStates.set(element, managedStyles);
|
|
500
|
+
}
|
|
350
501
|
}
|
|
351
502
|
const pointers = /* @__PURE__ */ new Map();
|
|
352
503
|
/**
|
|
@@ -356,10 +507,14 @@ function createDrag(element, options = {}) {
|
|
|
356
507
|
*/
|
|
357
508
|
const targets = /* @__PURE__ */ new Map();
|
|
358
509
|
let previousCursor;
|
|
510
|
+
/** The pointer that asked for the lock, while it is still down. */
|
|
511
|
+
let lockedPointerId = null;
|
|
512
|
+
let lockRequest = 0;
|
|
513
|
+
let destroyed = false;
|
|
359
514
|
function state(event, pointer, deltaX, deltaY) {
|
|
360
515
|
return {
|
|
361
|
-
x: event.screenX - pointer.startX,
|
|
362
|
-
y: event.screenY - pointer.startY,
|
|
516
|
+
x: pointer.lockBaseX !== void 0 ? pointer.lockBaseX + pointer.lockMoveX : event.screenX - pointer.startX,
|
|
517
|
+
y: pointer.lockBaseY !== void 0 ? pointer.lockBaseY + pointer.lockMoveY : event.screenY - pointer.startY,
|
|
363
518
|
deltaX,
|
|
364
519
|
deltaY,
|
|
365
520
|
clientX: event.clientX,
|
|
@@ -382,6 +537,7 @@ function createDrag(element, options = {}) {
|
|
|
382
537
|
target.addEventListener("pointermove", handlePointerMove);
|
|
383
538
|
target.addEventListener("pointerup", handlePointerUp);
|
|
384
539
|
target.addEventListener("pointercancel", handlePointerUp);
|
|
540
|
+
if (target === element) target.addEventListener("lostpointercapture", handleLostPointerCapture);
|
|
385
541
|
}
|
|
386
542
|
targets.set(target, count + 1);
|
|
387
543
|
}
|
|
@@ -394,13 +550,16 @@ function createDrag(element, options = {}) {
|
|
|
394
550
|
target.removeEventListener("pointermove", handlePointerMove);
|
|
395
551
|
target.removeEventListener("pointerup", handlePointerUp);
|
|
396
552
|
target.removeEventListener("pointercancel", handlePointerUp);
|
|
553
|
+
if (target === element) target.removeEventListener("lostpointercapture", handleLostPointerCapture);
|
|
397
554
|
targets.delete(target);
|
|
398
555
|
}
|
|
399
556
|
function handlePointerDown(event) {
|
|
400
557
|
const pointerEvent = event;
|
|
558
|
+
if (pointerEvent.button !== 0) return;
|
|
401
559
|
const pointerId = pointerEvent.pointerId;
|
|
402
560
|
if (pointers.has(pointerId)) return;
|
|
403
561
|
if (!multiPointer && pointers.size > 0) return;
|
|
562
|
+
if (opts.shouldStart && !opts.shouldStart(pointerEvent)) return;
|
|
404
563
|
const isFirst = pointers.size === 0;
|
|
405
564
|
if (isFirst && opts.cursor && style) {
|
|
406
565
|
previousCursor = style.cursor;
|
|
@@ -413,39 +572,86 @@ function createDrag(element, options = {}) {
|
|
|
413
572
|
startX: pointerEvent.screenX,
|
|
414
573
|
startY: pointerEvent.screenY,
|
|
415
574
|
lastX: pointerEvent.screenX,
|
|
416
|
-
lastY: pointerEvent.screenY
|
|
575
|
+
lastY: pointerEvent.screenY,
|
|
576
|
+
lockMoveX: 0,
|
|
577
|
+
lockMoveY: 0,
|
|
578
|
+
lastEvent: pointerEvent
|
|
417
579
|
};
|
|
418
580
|
pointers.set(pointerId, pointer);
|
|
419
581
|
retainTarget(moveTarget);
|
|
420
582
|
if (isFirst) globalThis.document?.addEventListener("selectstart", preventSelectStart);
|
|
583
|
+
if (isFirst && opts.pointerLock) {
|
|
584
|
+
const requestId = ++lockRequest;
|
|
585
|
+
lockedPointerId = pointerId;
|
|
586
|
+
globalThis.document?.addEventListener("pointerlockchange", handleLockChange);
|
|
587
|
+
try {
|
|
588
|
+
(capture.requestPointerLock?.())?.then?.(() => {
|
|
589
|
+
if (requestId === lockRequest && (!pointers.has(pointerId) || destroyed) && globalThis.document?.pointerLockElement === element) globalThis.document.exitPointerLock?.();
|
|
590
|
+
}, () => {});
|
|
591
|
+
} catch {}
|
|
592
|
+
}
|
|
421
593
|
opts.onDragStart?.(state(pointerEvent, pointer, 0, 0));
|
|
422
594
|
}
|
|
423
595
|
function handlePointerMove(event) {
|
|
424
596
|
const pointerEvent = event;
|
|
425
597
|
const pointer = pointers.get(pointerEvent.pointerId);
|
|
426
598
|
if (!pointer) return;
|
|
427
|
-
const
|
|
428
|
-
const
|
|
599
|
+
const locked = pointer.lockBaseX !== void 0;
|
|
600
|
+
const deltaX = locked ? pointerEvent.movementX ?? 0 : pointerEvent.screenX - pointer.lastX;
|
|
601
|
+
const deltaY = locked ? pointerEvent.movementY ?? 0 : pointerEvent.screenY - pointer.lastY;
|
|
429
602
|
const threshold = opts.threshold ?? 0;
|
|
430
603
|
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
|
|
431
604
|
pointer.lastX = pointerEvent.screenX;
|
|
432
605
|
pointer.lastY = pointerEvent.screenY;
|
|
606
|
+
pointer.lastEvent = pointerEvent;
|
|
607
|
+
if (locked) {
|
|
608
|
+
pointer.lockMoveX += deltaX;
|
|
609
|
+
pointer.lockMoveY += deltaY;
|
|
610
|
+
}
|
|
433
611
|
opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY));
|
|
434
612
|
}
|
|
613
|
+
function handleLockChange() {
|
|
614
|
+
if (lockedPointerId === null) return;
|
|
615
|
+
const pointer = pointers.get(lockedPointerId);
|
|
616
|
+
if (!pointer) return;
|
|
617
|
+
if (globalThis.document?.pointerLockElement === element) {
|
|
618
|
+
pointer.lockBaseX = pointer.lastX - pointer.startX;
|
|
619
|
+
pointer.lockBaseY = pointer.lastY - pointer.startY;
|
|
620
|
+
pointer.lockMoveX = 0;
|
|
621
|
+
pointer.lockMoveY = 0;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (pointer.lockBaseX === void 0) return;
|
|
625
|
+
finishDrag(lockedPointerId);
|
|
626
|
+
}
|
|
435
627
|
function handlePointerUp(event) {
|
|
436
628
|
const pointerEvent = event;
|
|
437
|
-
|
|
629
|
+
finishDrag(pointerEvent.pointerId, pointerEvent);
|
|
630
|
+
}
|
|
631
|
+
function handleLostPointerCapture(event) {
|
|
632
|
+
finishDrag(event.pointerId);
|
|
633
|
+
}
|
|
634
|
+
function finishDrag(pointerId, event) {
|
|
635
|
+
const pointer = pointers.get(pointerId);
|
|
438
636
|
if (!pointer) return;
|
|
439
|
-
const finalState = state(
|
|
440
|
-
stopTracking(
|
|
637
|
+
const finalState = event ? state(event, pointer, event.screenX - pointer.lastX, event.screenY - pointer.lastY) : state(pointer.lastEvent, pointer, 0, 0);
|
|
638
|
+
stopTracking(pointerId);
|
|
441
639
|
opts.onDragEnd?.(finalState);
|
|
442
640
|
}
|
|
443
641
|
function stopTracking(pointerId) {
|
|
444
642
|
const pointer = pointers.get(pointerId);
|
|
445
643
|
if (!pointer) return;
|
|
446
|
-
capture.releasePointerCapture?.(pointerId);
|
|
447
|
-
releaseTarget(pointer.moveTarget);
|
|
448
644
|
pointers.delete(pointerId);
|
|
645
|
+
releaseTarget(pointer.moveTarget);
|
|
646
|
+
try {
|
|
647
|
+
if (capture.hasPointerCapture?.(pointerId) === true) capture.releasePointerCapture?.(pointerId);
|
|
648
|
+
} catch {}
|
|
649
|
+
if (lockedPointerId === pointerId) {
|
|
650
|
+
lockedPointerId = null;
|
|
651
|
+
const document = globalThis.document;
|
|
652
|
+
document?.removeEventListener("pointerlockchange", handleLockChange);
|
|
653
|
+
if (document?.pointerLockElement === element) document.exitPointerLock?.();
|
|
654
|
+
}
|
|
449
655
|
if (pointers.size > 0) return;
|
|
450
656
|
globalThis.document?.removeEventListener("selectstart", preventSelectStart);
|
|
451
657
|
if (previousCursor !== void 0 && style) {
|
|
@@ -463,18 +669,23 @@ function createDrag(element, options = {}) {
|
|
|
463
669
|
};
|
|
464
670
|
},
|
|
465
671
|
destroy: () => {
|
|
466
|
-
|
|
672
|
+
if (destroyed) return;
|
|
673
|
+
destroyed = true;
|
|
467
674
|
element.removeEventListener("pointerdown", handlePointerDown);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
675
|
+
for (const pointerId of [...pointers.keys()]) finishDrag(pointerId);
|
|
676
|
+
if (style && managedStyles && --managedStyles.count === 0) {
|
|
677
|
+
for (const [property] of MANAGED_STYLES) {
|
|
678
|
+
const previous = managedStyles.previous.get(property);
|
|
679
|
+
if (previous?.value) style.setProperty(property, previous.value, previous.priority);
|
|
680
|
+
else style.removeProperty(property);
|
|
681
|
+
}
|
|
682
|
+
managedStyleStates.delete(element);
|
|
472
683
|
}
|
|
473
684
|
}
|
|
474
685
|
};
|
|
475
686
|
}
|
|
476
687
|
//#endregion
|
|
477
|
-
//#region src/piano/
|
|
688
|
+
//#region src/piano/index.ts
|
|
478
689
|
const DEFAULT_SOURCE = "api";
|
|
479
690
|
/**
|
|
480
691
|
* Drive a piano keyboard with pointers, and own what is sounding.
|
|
@@ -496,17 +707,19 @@ function createPianoInput(element, options) {
|
|
|
496
707
|
return [...held.keys()].sort((a, b) => a - b);
|
|
497
708
|
}
|
|
498
709
|
function noteOn(note, { source = DEFAULT_SOURCE, velocity } = {}) {
|
|
710
|
+
if (!Number.isInteger(note) || note < 0 || note > 127) throw new RangeError("note: requirements: an integer from 0 to 127");
|
|
499
711
|
if (note > (opts.midiMax ?? 127)) return;
|
|
500
712
|
const sources = held.get(note);
|
|
501
713
|
if (sources) {
|
|
502
714
|
sources.add(source);
|
|
503
715
|
return;
|
|
504
716
|
}
|
|
505
|
-
held.set(note, new Set([source]));
|
|
717
|
+
held.set(note, /* @__PURE__ */ new Set([source]));
|
|
506
718
|
opts.onPlayNote?.(note, velocity);
|
|
507
719
|
opts.onActiveNotesChange?.(activeNotes());
|
|
508
720
|
}
|
|
509
721
|
function noteOff(note, { source = DEFAULT_SOURCE } = {}) {
|
|
722
|
+
if (!Number.isInteger(note) || note < 0 || note > 127) throw new RangeError("note: requirements: an integer from 0 to 127");
|
|
510
723
|
const sources = held.get(note);
|
|
511
724
|
if (!sources) return;
|
|
512
725
|
sources.delete(source);
|
|
@@ -526,7 +739,7 @@ function createPianoInput(element, options) {
|
|
|
526
739
|
const previous = pointerNotes.get(pointerId);
|
|
527
740
|
if (previous === note) return;
|
|
528
741
|
if (previous !== void 0) noteOff(previous, { source });
|
|
529
|
-
if (note === null) pointerNotes.delete(pointerId);
|
|
742
|
+
if (note === null || note > (opts.midiMax ?? 127)) pointerNotes.delete(pointerId);
|
|
530
743
|
else {
|
|
531
744
|
pointerNotes.set(pointerId, note);
|
|
532
745
|
noteOn(note, { source });
|
|
@@ -547,18 +760,28 @@ function createPianoInput(element, options) {
|
|
|
547
760
|
...opts,
|
|
548
761
|
...next
|
|
549
762
|
};
|
|
763
|
+
const midiMax = opts.midiMax ?? 127;
|
|
764
|
+
const stoppedNotes = activeNotes().filter((note) => note > midiMax);
|
|
765
|
+
if (stoppedNotes.length === 0) return;
|
|
766
|
+
for (const note of stoppedNotes) {
|
|
767
|
+
held.delete(note);
|
|
768
|
+
opts.onStopNote?.(note);
|
|
769
|
+
}
|
|
770
|
+
for (const [pointerId, note] of pointerNotes) if (note > midiMax) pointerNotes.delete(pointerId);
|
|
771
|
+
opts.onActiveNotesChange?.(activeNotes());
|
|
550
772
|
},
|
|
551
773
|
noteOn,
|
|
552
774
|
noteOff,
|
|
553
775
|
activeNotes,
|
|
554
776
|
destroy: () => {
|
|
555
777
|
drag.destroy();
|
|
556
|
-
|
|
778
|
+
const notes = activeNotes();
|
|
779
|
+
for (const note of notes) {
|
|
557
780
|
held.delete(note);
|
|
558
781
|
opts.onStopNote?.(note);
|
|
559
782
|
}
|
|
560
783
|
pointerNotes.clear();
|
|
561
|
-
opts.onActiveNotesChange?.([]);
|
|
784
|
+
if (notes.length > 0) opts.onActiveNotesChange?.([]);
|
|
562
785
|
}
|
|
563
786
|
};
|
|
564
787
|
}
|
|
@@ -581,7 +804,7 @@ function toXY(value) {
|
|
|
581
804
|
return isPair(value) ? [value[0], value[1]] : [value, value];
|
|
582
805
|
}
|
|
583
806
|
//#endregion
|
|
584
|
-
//#region src/pointer/
|
|
807
|
+
//#region src/pointer/drag-value.ts
|
|
585
808
|
/**
|
|
586
809
|
* Map the pointer onto the bounding rect of an element: the value *is* the
|
|
587
810
|
* position pointed at, so the middle of the element is 0.5.
|
|
@@ -589,16 +812,43 @@ function toXY(value) {
|
|
|
589
812
|
* The element is read on every event, so it may be mounted after the drag is
|
|
590
813
|
* set up and may change size while a drag is in progress.
|
|
591
814
|
*/
|
|
592
|
-
function elementMapping(getElement) {
|
|
815
|
+
function elementMapping(getElement, { sensitivity } = {}) {
|
|
816
|
+
/** The reported position when the sensitivity last changed. */
|
|
817
|
+
let origin = [0, 0];
|
|
818
|
+
/** The raw position at that same moment. */
|
|
819
|
+
let anchor = [0, 0];
|
|
820
|
+
/** The previous event, so a sensitivity change can be dated back to it. */
|
|
821
|
+
let previous = [0, 0];
|
|
822
|
+
let factor = 1;
|
|
593
823
|
function positionIn(state) {
|
|
594
824
|
const element = getElement();
|
|
595
825
|
if (!element) return null;
|
|
596
826
|
const { left, top, right, bottom } = element.getBoundingClientRect();
|
|
597
827
|
return [right > left ? (0, _tremolo_ui_functions.normalizeValue)(state.clientX, left, right) : 0, bottom > top ? (0, _tremolo_ui_functions.normalizeValue)(state.clientY, top, bottom) : 0];
|
|
598
828
|
}
|
|
829
|
+
const reported = (raw, at) => [origin[0] + (raw[0] - anchor[0]) * at, origin[1] + (raw[1] - anchor[1]) * at];
|
|
599
830
|
return {
|
|
600
|
-
start:
|
|
601
|
-
|
|
831
|
+
start: (state) => {
|
|
832
|
+
const raw = positionIn(state);
|
|
833
|
+
if (!raw) return null;
|
|
834
|
+
origin = raw;
|
|
835
|
+
anchor = raw;
|
|
836
|
+
previous = raw;
|
|
837
|
+
factor = sensitivity ? sensitivity(state) : 1;
|
|
838
|
+
return raw;
|
|
839
|
+
},
|
|
840
|
+
move: (state) => {
|
|
841
|
+
const raw = positionIn(state);
|
|
842
|
+
if (!raw) return null;
|
|
843
|
+
const next = sensitivity ? sensitivity(state) : 1;
|
|
844
|
+
if (next !== factor) {
|
|
845
|
+
origin = reported(previous, factor);
|
|
846
|
+
anchor = previous;
|
|
847
|
+
factor = next;
|
|
848
|
+
}
|
|
849
|
+
previous = raw;
|
|
850
|
+
return reported(raw, factor);
|
|
851
|
+
}
|
|
602
852
|
};
|
|
603
853
|
}
|
|
604
854
|
/**
|
|
@@ -606,15 +856,33 @@ function elementMapping(getElement) {
|
|
|
606
856
|
* distance dragged. The pointer position itself carries no meaning, so the
|
|
607
857
|
* value can be adjusted from anywhere on the screen.
|
|
608
858
|
*/
|
|
609
|
-
function relativeMapping({ pixelRange = 100 } = {}) {
|
|
610
|
-
const [
|
|
859
|
+
function relativeMapping({ pixelRange = 100, sensitivity } = {}) {
|
|
860
|
+
const [baseX, baseY] = toXY(pixelRange);
|
|
611
861
|
let origin = [0, 0];
|
|
862
|
+
/** Where the current sensitivity took over, in drag coordinates. */
|
|
863
|
+
let anchor = [0, 0];
|
|
864
|
+
/** The previous event, so a sensitivity change can be dated back to it. */
|
|
865
|
+
let previous = [0, 0];
|
|
866
|
+
let factor = 1;
|
|
867
|
+
const travelled = (to, at) => [baseX === 0 ? origin[0] : origin[0] + (to[0] - anchor[0]) * at / baseX, baseY === 0 ? origin[1] : origin[1] + (to[1] - anchor[1]) * at / baseY];
|
|
612
868
|
return {
|
|
613
|
-
start: (
|
|
869
|
+
start: (state, context) => {
|
|
614
870
|
origin = context.position();
|
|
871
|
+
anchor = [0, 0];
|
|
872
|
+
previous = [0, 0];
|
|
873
|
+
factor = sensitivity ? sensitivity(state) : 1;
|
|
615
874
|
return origin;
|
|
616
875
|
},
|
|
617
|
-
move: (state) =>
|
|
876
|
+
move: (state) => {
|
|
877
|
+
const next = sensitivity ? sensitivity(state) : 1;
|
|
878
|
+
if (next !== factor) {
|
|
879
|
+
origin = travelled(previous, factor);
|
|
880
|
+
anchor = previous;
|
|
881
|
+
factor = next;
|
|
882
|
+
}
|
|
883
|
+
previous = [state.x, state.y];
|
|
884
|
+
return travelled([state.x, state.y], factor);
|
|
885
|
+
}
|
|
618
886
|
};
|
|
619
887
|
}
|
|
620
888
|
/**
|
|
@@ -627,12 +895,14 @@ function relativeMapping({ pixelRange = 100 } = {}) {
|
|
|
627
895
|
function createDragValue(element, options) {
|
|
628
896
|
let opts = options;
|
|
629
897
|
let lastValue = [0, 0];
|
|
898
|
+
let active = false;
|
|
630
899
|
const axes = () => toXY(opts.axis);
|
|
631
900
|
function valueOf(position) {
|
|
632
901
|
return axes().map((axis, i) => {
|
|
633
902
|
const p = axis.reverse ? 1 - position[i] : position[i];
|
|
634
903
|
const value = (axis.scale ?? _tremolo_ui_functions.linearScale).denormalize(p, axis.min, axis.max);
|
|
635
|
-
|
|
904
|
+
const stepped = axis.step ? (0, _tremolo_ui_functions.stepValue)(value, axis.step) : value;
|
|
905
|
+
return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.toPrecision)(stepped), axis.min, axis.max);
|
|
636
906
|
});
|
|
637
907
|
}
|
|
638
908
|
const context = { position: () => {
|
|
@@ -647,21 +917,35 @@ function createDragValue(element, options) {
|
|
|
647
917
|
const drag = createDrag(element, {
|
|
648
918
|
threshold: opts.threshold,
|
|
649
919
|
cursor: opts.cursor,
|
|
920
|
+
pointerLock: opts.pointerLock,
|
|
921
|
+
shouldStart: (event) => opts.shouldStart?.(event) ?? true,
|
|
650
922
|
onDragStart: (state) => {
|
|
651
923
|
const position = opts.mapping.start(state, context);
|
|
652
|
-
if (position)
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
924
|
+
if (!position) return;
|
|
925
|
+
active = true;
|
|
926
|
+
lastValue = valueOf(position);
|
|
927
|
+
if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state);
|
|
656
928
|
opts.onDragStart?.(lastValue, state);
|
|
657
929
|
},
|
|
658
930
|
onDrag: (state) => {
|
|
931
|
+
if (!active) return;
|
|
659
932
|
const position = opts.mapping.move(state, context);
|
|
660
933
|
if (!position) return;
|
|
661
934
|
lastValue = valueOf(position);
|
|
662
935
|
opts.onChange?.(lastValue, state);
|
|
663
936
|
},
|
|
664
|
-
onDragEnd: (state) =>
|
|
937
|
+
onDragEnd: (state) => {
|
|
938
|
+
if (!active) return;
|
|
939
|
+
if (state.event.type === "pointerup" && (state.deltaX !== 0 || state.deltaY !== 0)) {
|
|
940
|
+
const position = opts.mapping.move(state, context);
|
|
941
|
+
if (position) {
|
|
942
|
+
lastValue = valueOf(position);
|
|
943
|
+
opts.onChange?.(lastValue, state);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
active = false;
|
|
947
|
+
opts.onDragEnd?.(lastValue, state);
|
|
948
|
+
}
|
|
665
949
|
});
|
|
666
950
|
return {
|
|
667
951
|
update: (next) => {
|
|
@@ -672,7 +956,8 @@ function createDragValue(element, options) {
|
|
|
672
956
|
};
|
|
673
957
|
drag.update({
|
|
674
958
|
threshold: opts.threshold,
|
|
675
|
-
cursor: opts.cursor
|
|
959
|
+
cursor: opts.cursor,
|
|
960
|
+
pointerLock: opts.pointerLock
|
|
676
961
|
});
|
|
677
962
|
},
|
|
678
963
|
destroy: () => drag.destroy()
|
|
@@ -687,14 +972,17 @@ function createDragValue(element, options) {
|
|
|
687
972
|
* `preventDefault()` to stop the page from scrolling.
|
|
688
973
|
*/
|
|
689
974
|
function createWheel(element, onWheel, options = {}) {
|
|
690
|
-
let opts =
|
|
975
|
+
let opts = {
|
|
976
|
+
...options,
|
|
977
|
+
onWheel
|
|
978
|
+
};
|
|
691
979
|
function hasFocus() {
|
|
692
980
|
const active = element.ownerDocument?.activeElement;
|
|
693
981
|
return !!active && element.contains(active);
|
|
694
982
|
}
|
|
695
983
|
const handler = (event) => {
|
|
696
984
|
if (opts.requireFocus && !hasFocus()) return;
|
|
697
|
-
onWheel(event);
|
|
985
|
+
opts.onWheel?.(event);
|
|
698
986
|
};
|
|
699
987
|
element.addEventListener("wheel", handler, { passive: false });
|
|
700
988
|
return {
|
|
@@ -710,8 +998,77 @@ function createWheel(element, onWheel, options = {}) {
|
|
|
710
998
|
};
|
|
711
999
|
}
|
|
712
1000
|
//#endregion
|
|
1001
|
+
//#region src/selection/box.ts
|
|
1002
|
+
function rectOf(from, to) {
|
|
1003
|
+
return {
|
|
1004
|
+
x: Math.min(from[0], to[0]),
|
|
1005
|
+
y: Math.min(from[1], to[1]),
|
|
1006
|
+
width: Math.abs(to[0] - from[0]),
|
|
1007
|
+
height: Math.abs(to[1] - from[1])
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
/** Whether the box covers the point, edges included. */
|
|
1011
|
+
function selectionBoxCovers(box, [x, y]) {
|
|
1012
|
+
return x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Selecting by dragging a box over a set of items.
|
|
1016
|
+
*
|
|
1017
|
+
* The drag itself is not here: the caller owns the pointer, and reports where
|
|
1018
|
+
* it went in the same 0..1 space the items are given in.
|
|
1019
|
+
*/
|
|
1020
|
+
function createSelectionBox(options) {
|
|
1021
|
+
let opts = options;
|
|
1022
|
+
let drag = null;
|
|
1023
|
+
let box = null;
|
|
1024
|
+
function setBox(next) {
|
|
1025
|
+
box = next;
|
|
1026
|
+
opts.onBoxChange?.(next);
|
|
1027
|
+
}
|
|
1028
|
+
function apply(rect, base) {
|
|
1029
|
+
const inside = [];
|
|
1030
|
+
for (const [id, at] of opts.items()) if (selectionBoxCovers(rect, at) && !base.includes(id)) inside.push(id);
|
|
1031
|
+
opts.onSelectionChange?.([...base, ...inside]);
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
update: (next) => {
|
|
1035
|
+
opts = {
|
|
1036
|
+
...opts,
|
|
1037
|
+
...next
|
|
1038
|
+
};
|
|
1039
|
+
},
|
|
1040
|
+
begin: (at, { additive = false, selection = [] } = {}) => {
|
|
1041
|
+
drag = {
|
|
1042
|
+
from: at,
|
|
1043
|
+
base: additive ? [...selection] : []
|
|
1044
|
+
};
|
|
1045
|
+
setBox(rectOf(at, at));
|
|
1046
|
+
if (!additive) opts.onSelectionChange?.([]);
|
|
1047
|
+
},
|
|
1048
|
+
move: (to) => {
|
|
1049
|
+
if (!drag) return;
|
|
1050
|
+
const rect = rectOf(drag.from, to);
|
|
1051
|
+
setBox(rect);
|
|
1052
|
+
apply(rect, drag.base);
|
|
1053
|
+
},
|
|
1054
|
+
end: () => {
|
|
1055
|
+
const running = drag !== null;
|
|
1056
|
+
drag = null;
|
|
1057
|
+
if (box !== null) setBox(null);
|
|
1058
|
+
return running;
|
|
1059
|
+
},
|
|
1060
|
+
box: () => box,
|
|
1061
|
+
destroy: () => {
|
|
1062
|
+
drag = null;
|
|
1063
|
+
box = null;
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
//#endregion
|
|
713
1068
|
exports.NOT_SUPPORTED = NOT_SUPPORTED;
|
|
714
1069
|
exports.PERMISSION_DENIED = PERMISSION_DENIED;
|
|
1070
|
+
exports.PITCH_BEND_CENTER = PITCH_BEND_CENTER;
|
|
1071
|
+
exports.UNAVAILABLE = UNAVAILABLE;
|
|
715
1072
|
exports.createAnimationCanvas = createAnimationCanvas;
|
|
716
1073
|
exports.createDrag = createDrag;
|
|
717
1074
|
exports.createDragValue = createDragValue;
|
|
@@ -719,11 +1076,13 @@ exports.createMIDIAccess = createMIDIAccess;
|
|
|
719
1076
|
exports.createMIDIInput = createMIDIInput;
|
|
720
1077
|
exports.createMIDIMessage = createMIDIMessage;
|
|
721
1078
|
exports.createPianoInput = createPianoInput;
|
|
1079
|
+
exports.createSelectionBox = createSelectionBox;
|
|
722
1080
|
exports.createWheel = createWheel;
|
|
723
1081
|
exports.drawingState = drawingState;
|
|
724
1082
|
exports.elementMapping = elementMapping;
|
|
725
1083
|
exports.isDrawingState = isDrawingState;
|
|
726
1084
|
exports.relativeMapping = relativeMapping;
|
|
1085
|
+
exports.selectionBoxCovers = selectionBoxCovers;
|
|
727
1086
|
exports.toXY = toXY;
|
|
728
1087
|
|
|
729
1088
|
//# sourceMappingURL=index.cjs.map
|