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