@tremolo-ui/dom 0.7.0 → 0.9.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 +343 -595
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +230 -284
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +230 -284
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +325 -572
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +530 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +365 -0
- package/dist/internal.d.cts.map +1 -0
- package/dist/internal.d.ts +365 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +500 -0
- package/dist/internal.js.map +1 -0
- package/dist/marks-DK7xJmGM.d.cts +305 -0
- package/dist/marks-DK7xJmGM.d.cts.map +1 -0
- package/dist/marks-DK7xJmGM.d.ts +305 -0
- package/dist/marks-DK7xJmGM.d.ts.map +1 -0
- package/dist/xy-5Oc6JeJr.cjs +952 -0
- package/dist/xy-5Oc6JeJr.cjs.map +1 -0
- package/dist/xy-lNFl1CTO.js +827 -0
- package/dist/xy-lNFl1CTO.js.map +1 -0
- package/package.json +12 -2
- package/src/canvas/animation.ts +8 -0
- package/src/canvas/context.ts +0 -5
- package/src/file/accept.ts +17 -0
- package/src/file/drop-zone.ts +8 -10
- package/src/index.ts +44 -32
- package/src/input/change-gesture.ts +104 -0
- package/src/input/check-steps.ts +144 -0
- package/src/input/defaults.ts +32 -0
- package/src/input/direction.ts +107 -0
- package/src/internal.ts +51 -0
- package/src/knob/geometry.ts +100 -0
- package/src/midi/access.ts +16 -15
- package/src/midi/input.ts +2 -9
- package/src/number-input/stepper-drag.ts +147 -0
- package/src/number-input/text.ts +100 -0
- package/src/number-input/value.ts +129 -0
- package/src/options/replace.ts +22 -0
- package/src/piano/index.ts +128 -2
- package/src/piano/layout.ts +19 -0
- package/src/piano/shortcuts.ts +86 -0
- package/src/pointer/drag-value.ts +3 -1
- package/src/pointer/long-press.ts +103 -0
- package/src/pointer/wheel.ts +10 -7
- package/src/points-editor/index.ts +367 -0
- package/src/position.ts +20 -0
- package/src/slider/decimal-digits.ts +23 -0
- package/src/slider/marks.ts +56 -0
- package/src/style.ts +41 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import { clamp, isBlackKey, isWhiteKey, linearScale, noteKey, noteKeys, stepValue, toPrecision } from "@tremolo-ui/functions";
|
|
2
|
+
//#region src/file/accept.ts
|
|
3
|
+
/**
|
|
4
|
+
* Does a file satisfy an `accept` attribute?
|
|
5
|
+
*
|
|
6
|
+
* `accept` is written the way the HTML attribute is: a comma separated list of
|
|
7
|
+
* extensions (`.wav`), MIME types (`audio/wav`) and type groups (`audio/*`).
|
|
8
|
+
* Anything that matches one entry is accepted, and an empty or missing
|
|
9
|
+
* `accept` takes everything.
|
|
10
|
+
*
|
|
11
|
+
* **The browser's own `accept` is only a hint to the file picker.** A person
|
|
12
|
+
* can switch it to "All Files", drag a file in, or pick one the picker was
|
|
13
|
+
* never asked about, so what arrives still has to be checked.
|
|
14
|
+
*
|
|
15
|
+
* A rule that cannot be decided is not treated as a rejection: with no `name`,
|
|
16
|
+
* an extension rule says nothing either way, and `accept=".wav"` reports a
|
|
17
|
+
* match rather than refusing a file it has not seen the name of. The name is
|
|
18
|
+
* there by the time the file is dropped, which is when the answer counts.
|
|
19
|
+
*/
|
|
20
|
+
function matchesAccept(candidate, accept) {
|
|
21
|
+
const rules = (accept ?? "").split(",").map((rule) => rule.trim().toLowerCase()).filter(Boolean);
|
|
22
|
+
if (rules.length === 0) return true;
|
|
23
|
+
const name = candidate.name?.toLowerCase();
|
|
24
|
+
const type = candidate.type.toLowerCase();
|
|
25
|
+
let undecided = false;
|
|
26
|
+
for (const rule of rules) if (rule.startsWith(".")) {
|
|
27
|
+
if (name === void 0) undecided = true;
|
|
28
|
+
else if (name.endsWith(rule)) return true;
|
|
29
|
+
} else if (rule.endsWith("/*")) {
|
|
30
|
+
if (type.startsWith(rule.slice(0, -1))) return true;
|
|
31
|
+
} else if (type === rule) return true;
|
|
32
|
+
return undecided;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Split files into those that satisfy `accept` and those that do not, keeping
|
|
36
|
+
* their order. See {@link matchesAccept}.
|
|
37
|
+
*/
|
|
38
|
+
function partitionByAccept(files, accept) {
|
|
39
|
+
const accepted = [];
|
|
40
|
+
const rejected = [];
|
|
41
|
+
for (const file of files) if (matchesAccept(file, accept)) accepted.push(file);
|
|
42
|
+
else rejected.push(file);
|
|
43
|
+
return {
|
|
44
|
+
accepted,
|
|
45
|
+
rejected
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/input/defaults.ts
|
|
50
|
+
/**
|
|
51
|
+
* The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
|
|
52
|
+
* 1 per press in the units of the value, and 0.1 with shift held. That is one
|
|
53
|
+
* `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
|
|
54
|
+
* `checkSteps` warns about.
|
|
55
|
+
*
|
|
56
|
+
* A modifier entry is not snapped to `step`, which is what lets the finer
|
|
57
|
+
* amount move at all — see `applyDelta`.
|
|
58
|
+
*/
|
|
59
|
+
const DEFAULT_KEYBOARD_OPTIONS = {
|
|
60
|
+
default: ["raw", 1],
|
|
61
|
+
shift: ["raw", .1]
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
|
|
65
|
+
*
|
|
66
|
+
* Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
|
|
67
|
+
* and fills `deltaX`, so no modifier is bound here.
|
|
68
|
+
*/
|
|
69
|
+
const DEFAULT_WHEEL_OPTIONS = ["raw", 1];
|
|
70
|
+
/**
|
|
71
|
+
* The drag sensitivity used by value controls by default. Shift makes the
|
|
72
|
+
* same movement cover a tenth of the range, matching the arrow keys.
|
|
73
|
+
*/
|
|
74
|
+
const DEFAULT_DRAG_SENSITIVITY = {
|
|
75
|
+
default: 1,
|
|
76
|
+
shift: .1
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/piano/layout.ts
|
|
80
|
+
/**
|
|
81
|
+
* `[noteRange.first, noteRange.first + 1, ..., noteRange.last]`
|
|
82
|
+
*/
|
|
83
|
+
function getNoteRangeArray(noteRange) {
|
|
84
|
+
return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
|
|
85
|
+
}
|
|
86
|
+
const DEFAULT_KEY_GAP = 1;
|
|
87
|
+
const DEFAULT_BLACK_KEY_WIDTH_RATIO = .65;
|
|
88
|
+
const DEFAULT_BLACK_KEY_HEIGHT_RATIO = .6;
|
|
89
|
+
/**
|
|
90
|
+
* How many white keys sit at or before each pitch class, counting from C.
|
|
91
|
+
*
|
|
92
|
+
* A black key shares the number of the white key to its left plus one, which
|
|
93
|
+
* puts it on the boundary between the two; {@link notePosition} then shifts it
|
|
94
|
+
* back by half its width to centre it there.
|
|
95
|
+
*/
|
|
96
|
+
const whiteKeysBefore = {
|
|
97
|
+
C: 0,
|
|
98
|
+
"C#": 1,
|
|
99
|
+
D: 1,
|
|
100
|
+
"D#": 2,
|
|
101
|
+
E: 2,
|
|
102
|
+
F: 3,
|
|
103
|
+
"F#": 4,
|
|
104
|
+
G: 4,
|
|
105
|
+
"G#": 5,
|
|
106
|
+
A: 5,
|
|
107
|
+
"A#": 6,
|
|
108
|
+
B: 6
|
|
109
|
+
};
|
|
110
|
+
/** Width of a black key in pixels. */
|
|
111
|
+
function blackKeyWidth(layout) {
|
|
112
|
+
return layout.whiteKeyWidth * (layout.blackKeyWidthRatio ?? DEFAULT_BLACK_KEY_WIDTH_RATIO);
|
|
113
|
+
}
|
|
114
|
+
function rawNotePosition(note, layout) {
|
|
115
|
+
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
|
|
116
|
+
const target = noteKey(note);
|
|
117
|
+
const first = noteKey(layout.noteRange.first);
|
|
118
|
+
const octave = Math.floor((note - layout.noteRange.first) / 12);
|
|
119
|
+
const octaveOffset = noteKeys.indexOf(first) > noteKeys.indexOf(target) ? 1 : 0;
|
|
120
|
+
const whiteKeysIn = whiteKeysBefore[target] - whiteKeysBefore[first] + (octave + octaveOffset) * 7;
|
|
121
|
+
return isBlackKey(note) ? whiteKeysIn * slot - blackKeyWidth(layout) / 2 : whiteKeysIn * slot;
|
|
122
|
+
}
|
|
123
|
+
function pianoBounds(layout) {
|
|
124
|
+
const notes = getNoteRangeArray(layout.noteRange);
|
|
125
|
+
if (notes.length === 0) return {
|
|
126
|
+
left: 0,
|
|
127
|
+
right: 0
|
|
128
|
+
};
|
|
129
|
+
let left = Infinity;
|
|
130
|
+
let right = -Infinity;
|
|
131
|
+
const whiteWidth = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
|
|
132
|
+
for (const note of notes) {
|
|
133
|
+
const noteLeft = rawNotePosition(note, layout);
|
|
134
|
+
const width = isBlackKey(note) ? blackKeyWidth(layout) : whiteWidth;
|
|
135
|
+
left = Math.min(left, noteLeft);
|
|
136
|
+
right = Math.max(right, noteLeft + width);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
left,
|
|
140
|
+
right
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Width of the whole keyboard in pixels. */
|
|
144
|
+
function pianoWidth(layout) {
|
|
145
|
+
const { left, right } = pianoBounds(layout);
|
|
146
|
+
return right - left;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Offset of the left edge of a key from the left edge of the keyboard, in
|
|
150
|
+
* pixels.
|
|
151
|
+
*
|
|
152
|
+
* Notes outside `noteRange` are placed too, so the value is negative below
|
|
153
|
+
* `noteRange.first`.
|
|
154
|
+
*/
|
|
155
|
+
function notePosition(note, layout) {
|
|
156
|
+
return rawNotePosition(note, layout) - pianoBounds(layout).left;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The note drawn at a point, or null where there is none.
|
|
160
|
+
*
|
|
161
|
+
* Black keys are tested first, so they win where they overlap a white one. A
|
|
162
|
+
* white key covers its gap as well as its width, so the whole width of the
|
|
163
|
+
* keyboard belongs to some key and a click cannot fall between two.
|
|
164
|
+
*
|
|
165
|
+
* @param x offset from the left edge of the keyboard, in pixels
|
|
166
|
+
* @param y offset from its top edge, in pixels
|
|
167
|
+
* @param height height of the keyboard, in pixels
|
|
168
|
+
*/
|
|
169
|
+
function noteAt(x, y, height, layout) {
|
|
170
|
+
if (x < 0 || x >= pianoWidth(layout) || y < 0 || y >= height) return null;
|
|
171
|
+
const notes = getNoteRangeArray(layout.noteRange);
|
|
172
|
+
if (y < height * (layout.blackKeyHeightRatio ?? DEFAULT_BLACK_KEY_HEIGHT_RATIO)) for (const note of notes) {
|
|
173
|
+
if (isWhiteKey(note)) continue;
|
|
174
|
+
const left = notePosition(note, layout);
|
|
175
|
+
if (left <= x && x < left + blackKeyWidth(layout)) return note;
|
|
176
|
+
}
|
|
177
|
+
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
|
|
178
|
+
for (const note of notes) {
|
|
179
|
+
if (isBlackKey(note)) continue;
|
|
180
|
+
const left = notePosition(note, layout);
|
|
181
|
+
if (left <= x && x < left + slot) return note;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The white key width that makes the keyboard exactly `width` wide, for a
|
|
187
|
+
* keyboard that follows the size of its container.
|
|
188
|
+
*
|
|
189
|
+
* Solved from {@link pianoWidth} rather than by dividing among the white
|
|
190
|
+
* keys, so that a range that starts or ends on a black key — which sticks out
|
|
191
|
+
* by a fraction of a white key — still fills the container. The width grows
|
|
192
|
+
* linearly with the white key width, so two samples pin it down.
|
|
193
|
+
*/
|
|
194
|
+
function fitWhiteKeyWidth(width, layout) {
|
|
195
|
+
const at = (whiteKeyWidth) => pianoWidth({
|
|
196
|
+
...layout,
|
|
197
|
+
whiteKeyWidth
|
|
198
|
+
});
|
|
199
|
+
const slope = at(2) - at(1);
|
|
200
|
+
if (slope <= 0) return 0;
|
|
201
|
+
return (width - (at(1) - slope)) / slope;
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/pointer/drag.ts
|
|
205
|
+
/**
|
|
206
|
+
* Applied to the element for the lifetime of the instance.
|
|
207
|
+
*
|
|
208
|
+
* `touch-action` stops touch dragging from scrolling the page. That alone makes
|
|
209
|
+
* the browser treat a long press as the start of a text selection, so selection
|
|
210
|
+
* and the iOS callout are suppressed here too.
|
|
211
|
+
*/
|
|
212
|
+
const MANAGED_STYLES = [
|
|
213
|
+
["touch-action", "none"],
|
|
214
|
+
["user-select", "none"],
|
|
215
|
+
["-webkit-user-select", "none"],
|
|
216
|
+
["-webkit-touch-callout", "none"]
|
|
217
|
+
];
|
|
218
|
+
const managedStyleStates = /* @__PURE__ */ new WeakMap();
|
|
219
|
+
/**
|
|
220
|
+
* Track a pointer drag on an element.
|
|
221
|
+
*
|
|
222
|
+
* Uses Pointer Events only, and pointer capture so that the drag keeps working
|
|
223
|
+
* once the pointer leaves the element. For the lifetime of the instance the
|
|
224
|
+
* element gets `touch-action: none` so that touch dragging does not scroll the
|
|
225
|
+
* page, plus `user-select: none` so that a long press does not start a text
|
|
226
|
+
* selection instead.
|
|
227
|
+
*
|
|
228
|
+
* One pointer at a time by default; see {@link DragOptions.multiPointer}.
|
|
229
|
+
*/
|
|
230
|
+
function createDrag(element, options = {}) {
|
|
231
|
+
let opts = options;
|
|
232
|
+
const multiPointer = options.multiPointer ?? false;
|
|
233
|
+
const capture = element;
|
|
234
|
+
const style = element.style;
|
|
235
|
+
let managedStyles = managedStyleStates.get(element);
|
|
236
|
+
if (style) {
|
|
237
|
+
if (managedStyles) managedStyles.count += 1;
|
|
238
|
+
else {
|
|
239
|
+
const previous = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const [property, value] of MANAGED_STYLES) {
|
|
241
|
+
previous.set(property, {
|
|
242
|
+
value: style.getPropertyValue(property),
|
|
243
|
+
priority: style.getPropertyPriority(property)
|
|
244
|
+
});
|
|
245
|
+
style.setProperty(property, value);
|
|
246
|
+
}
|
|
247
|
+
managedStyles = {
|
|
248
|
+
count: 1,
|
|
249
|
+
previous
|
|
250
|
+
};
|
|
251
|
+
managedStyleStates.set(element, managedStyles);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
255
|
+
/**
|
|
256
|
+
* How many pointers each target carries. Adding the same listener twice is a
|
|
257
|
+
* no-op and removing it once removes it for good, so a target is subscribed
|
|
258
|
+
* to while at least one pointer is on it and no longer.
|
|
259
|
+
*/
|
|
260
|
+
const targets = /* @__PURE__ */ new Map();
|
|
261
|
+
let previousCursor;
|
|
262
|
+
/** The pointer that asked for the lock, while it is still down. */
|
|
263
|
+
let lockedPointerId = null;
|
|
264
|
+
let lockRequest = 0;
|
|
265
|
+
let destroyed = false;
|
|
266
|
+
function state(event, pointer, deltaX, deltaY) {
|
|
267
|
+
return {
|
|
268
|
+
x: pointer.lockBaseX !== void 0 ? pointer.lockBaseX + pointer.lockMoveX : event.screenX - pointer.startX,
|
|
269
|
+
y: pointer.lockBaseY !== void 0 ? pointer.lockBaseY + pointer.lockMoveY : event.screenY - pointer.startY,
|
|
270
|
+
deltaX,
|
|
271
|
+
deltaY,
|
|
272
|
+
clientX: event.clientX,
|
|
273
|
+
clientY: event.clientY,
|
|
274
|
+
pointerId: event.pointerId,
|
|
275
|
+
event
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* `user-select: none` only makes the element's own text unselectable; the
|
|
280
|
+
* browser still starts a selection on long press and grabs whatever text it
|
|
281
|
+
* finds nearby. Cancelling the selection outright is what actually stops it.
|
|
282
|
+
*/
|
|
283
|
+
function preventSelectStart(event) {
|
|
284
|
+
event.preventDefault();
|
|
285
|
+
}
|
|
286
|
+
function retainTarget(target) {
|
|
287
|
+
const count = targets.get(target) ?? 0;
|
|
288
|
+
if (count === 0) {
|
|
289
|
+
target.addEventListener("pointermove", handlePointerMove);
|
|
290
|
+
target.addEventListener("pointerup", handlePointerUp);
|
|
291
|
+
target.addEventListener("pointercancel", handlePointerUp);
|
|
292
|
+
if (target === element) target.addEventListener("lostpointercapture", handleLostPointerCapture);
|
|
293
|
+
}
|
|
294
|
+
targets.set(target, count + 1);
|
|
295
|
+
}
|
|
296
|
+
function releaseTarget(target) {
|
|
297
|
+
const count = targets.get(target) ?? 0;
|
|
298
|
+
if (count > 1) {
|
|
299
|
+
targets.set(target, count - 1);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
target.removeEventListener("pointermove", handlePointerMove);
|
|
303
|
+
target.removeEventListener("pointerup", handlePointerUp);
|
|
304
|
+
target.removeEventListener("pointercancel", handlePointerUp);
|
|
305
|
+
if (target === element) target.removeEventListener("lostpointercapture", handleLostPointerCapture);
|
|
306
|
+
targets.delete(target);
|
|
307
|
+
}
|
|
308
|
+
function handlePointerDown(event) {
|
|
309
|
+
const pointerEvent = event;
|
|
310
|
+
if (pointerEvent.button !== 0) return;
|
|
311
|
+
const pointerId = pointerEvent.pointerId;
|
|
312
|
+
if (pointers.has(pointerId)) return;
|
|
313
|
+
if (!multiPointer && pointers.size > 0) return;
|
|
314
|
+
if (opts.shouldStart && !opts.shouldStart(pointerEvent)) return;
|
|
315
|
+
const isFirst = pointers.size === 0;
|
|
316
|
+
if (isFirst && opts.cursor && style) {
|
|
317
|
+
previousCursor = style.cursor;
|
|
318
|
+
style.cursor = opts.cursor;
|
|
319
|
+
}
|
|
320
|
+
capture.setPointerCapture?.(pointerId);
|
|
321
|
+
const moveTarget = capture.hasPointerCapture?.(pointerId) === true ? element : globalThis.window ?? element;
|
|
322
|
+
const pointer = {
|
|
323
|
+
moveTarget,
|
|
324
|
+
startX: pointerEvent.screenX,
|
|
325
|
+
startY: pointerEvent.screenY,
|
|
326
|
+
lastX: pointerEvent.screenX,
|
|
327
|
+
lastY: pointerEvent.screenY,
|
|
328
|
+
lockMoveX: 0,
|
|
329
|
+
lockMoveY: 0,
|
|
330
|
+
lastEvent: pointerEvent
|
|
331
|
+
};
|
|
332
|
+
pointers.set(pointerId, pointer);
|
|
333
|
+
retainTarget(moveTarget);
|
|
334
|
+
if (isFirst) globalThis.document?.addEventListener("selectstart", preventSelectStart);
|
|
335
|
+
if (isFirst && opts.pointerLock) {
|
|
336
|
+
const requestId = ++lockRequest;
|
|
337
|
+
lockedPointerId = pointerId;
|
|
338
|
+
globalThis.document?.addEventListener("pointerlockchange", handleLockChange);
|
|
339
|
+
try {
|
|
340
|
+
(capture.requestPointerLock?.())?.then?.(() => {
|
|
341
|
+
if (requestId === lockRequest && (!pointers.has(pointerId) || destroyed) && globalThis.document?.pointerLockElement === element) globalThis.document.exitPointerLock?.();
|
|
342
|
+
}, () => {});
|
|
343
|
+
} catch {}
|
|
344
|
+
}
|
|
345
|
+
opts.onDragStart?.(state(pointerEvent, pointer, 0, 0));
|
|
346
|
+
}
|
|
347
|
+
function handlePointerMove(event) {
|
|
348
|
+
const pointerEvent = event;
|
|
349
|
+
const pointer = pointers.get(pointerEvent.pointerId);
|
|
350
|
+
if (!pointer) return;
|
|
351
|
+
const locked = pointer.lockBaseX !== void 0;
|
|
352
|
+
const deltaX = locked ? pointerEvent.movementX ?? 0 : pointerEvent.screenX - pointer.lastX;
|
|
353
|
+
const deltaY = locked ? pointerEvent.movementY ?? 0 : pointerEvent.screenY - pointer.lastY;
|
|
354
|
+
const threshold = opts.threshold ?? 0;
|
|
355
|
+
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
|
|
356
|
+
pointer.lastX = pointerEvent.screenX;
|
|
357
|
+
pointer.lastY = pointerEvent.screenY;
|
|
358
|
+
pointer.lastEvent = pointerEvent;
|
|
359
|
+
if (locked) {
|
|
360
|
+
pointer.lockMoveX += deltaX;
|
|
361
|
+
pointer.lockMoveY += deltaY;
|
|
362
|
+
}
|
|
363
|
+
opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY));
|
|
364
|
+
}
|
|
365
|
+
function handleLockChange() {
|
|
366
|
+
if (lockedPointerId === null) return;
|
|
367
|
+
const pointer = pointers.get(lockedPointerId);
|
|
368
|
+
if (!pointer) return;
|
|
369
|
+
if (globalThis.document?.pointerLockElement === element) {
|
|
370
|
+
pointer.lockBaseX = pointer.lastX - pointer.startX;
|
|
371
|
+
pointer.lockBaseY = pointer.lastY - pointer.startY;
|
|
372
|
+
pointer.lockMoveX = 0;
|
|
373
|
+
pointer.lockMoveY = 0;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (pointer.lockBaseX === void 0) return;
|
|
377
|
+
finishDrag(lockedPointerId);
|
|
378
|
+
}
|
|
379
|
+
function handlePointerUp(event) {
|
|
380
|
+
const pointerEvent = event;
|
|
381
|
+
finishDrag(pointerEvent.pointerId, pointerEvent);
|
|
382
|
+
}
|
|
383
|
+
function handleLostPointerCapture(event) {
|
|
384
|
+
finishDrag(event.pointerId);
|
|
385
|
+
}
|
|
386
|
+
function finishDrag(pointerId, event) {
|
|
387
|
+
const pointer = pointers.get(pointerId);
|
|
388
|
+
if (!pointer) return;
|
|
389
|
+
const finalState = event ? state(event, pointer, event.screenX - pointer.lastX, event.screenY - pointer.lastY) : state(pointer.lastEvent, pointer, 0, 0);
|
|
390
|
+
stopTracking(pointerId);
|
|
391
|
+
opts.onDragEnd?.(finalState);
|
|
392
|
+
}
|
|
393
|
+
function stopTracking(pointerId) {
|
|
394
|
+
const pointer = pointers.get(pointerId);
|
|
395
|
+
if (!pointer) return;
|
|
396
|
+
pointers.delete(pointerId);
|
|
397
|
+
releaseTarget(pointer.moveTarget);
|
|
398
|
+
try {
|
|
399
|
+
if (capture.hasPointerCapture?.(pointerId) === true) capture.releasePointerCapture?.(pointerId);
|
|
400
|
+
} catch {}
|
|
401
|
+
if (lockedPointerId === pointerId) {
|
|
402
|
+
lockedPointerId = null;
|
|
403
|
+
const document = globalThis.document;
|
|
404
|
+
document?.removeEventListener("pointerlockchange", handleLockChange);
|
|
405
|
+
if (document?.pointerLockElement === element) document.exitPointerLock?.();
|
|
406
|
+
}
|
|
407
|
+
if (pointers.size > 0) return;
|
|
408
|
+
globalThis.document?.removeEventListener("selectstart", preventSelectStart);
|
|
409
|
+
if (previousCursor !== void 0 && style) {
|
|
410
|
+
style.cursor = previousCursor;
|
|
411
|
+
previousCursor = void 0;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
element.addEventListener("pointerdown", handlePointerDown);
|
|
415
|
+
return {
|
|
416
|
+
update: (next) => {
|
|
417
|
+
opts = {
|
|
418
|
+
...opts,
|
|
419
|
+
...next,
|
|
420
|
+
multiPointer
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
destroy: () => {
|
|
424
|
+
if (destroyed) return;
|
|
425
|
+
destroyed = true;
|
|
426
|
+
element.removeEventListener("pointerdown", handlePointerDown);
|
|
427
|
+
for (const pointerId of [...pointers.keys()]) finishDrag(pointerId);
|
|
428
|
+
if (style && managedStyles && --managedStyles.count === 0) {
|
|
429
|
+
for (const [property] of MANAGED_STYLES) {
|
|
430
|
+
const previous = managedStyles.previous.get(property);
|
|
431
|
+
if (previous?.value) style.setProperty(property, previous.value, previous.priority);
|
|
432
|
+
else style.removeProperty(property);
|
|
433
|
+
}
|
|
434
|
+
managedStyleStates.delete(element);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/input/modifiers.ts
|
|
441
|
+
/**
|
|
442
|
+
* Checked in this order, and the first one that is both held and configured
|
|
443
|
+
* wins. Fixing an order is what keeps two modifiers held at once from
|
|
444
|
+
* behaving differently between browsers.
|
|
445
|
+
*/
|
|
446
|
+
const MODIFIER_ORDER = [
|
|
447
|
+
"meta",
|
|
448
|
+
"ctrl",
|
|
449
|
+
"alt",
|
|
450
|
+
"shift"
|
|
451
|
+
];
|
|
452
|
+
const MODIFIER_FLAG = {
|
|
453
|
+
meta: "metaKey",
|
|
454
|
+
ctrl: "ctrlKey",
|
|
455
|
+
alt: "altKey",
|
|
456
|
+
shift: "shiftKey"
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* A map is the only form with a `default` key, which is what tells it apart
|
|
460
|
+
* from a bare setting. Tuples are arrays, so they never match.
|
|
461
|
+
*/
|
|
462
|
+
function isModifierMap(value) {
|
|
463
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "default" in value;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Pick the setting that applies, given the modifier keys being held.
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* selectModifier({ default: 1, shift: 0.1 }, event)
|
|
470
|
+
*/
|
|
471
|
+
function selectModifier(options, modifiers) {
|
|
472
|
+
if (!isModifierMap(options)) return {
|
|
473
|
+
value: options,
|
|
474
|
+
modifier: null
|
|
475
|
+
};
|
|
476
|
+
if (modifiers) for (const modifier of MODIFIER_ORDER) {
|
|
477
|
+
const value = options[modifier];
|
|
478
|
+
if (value !== void 0 && modifiers[MODIFIER_FLAG[modifier]]) return {
|
|
479
|
+
value,
|
|
480
|
+
modifier
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
value: options.default,
|
|
485
|
+
modifier: null
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Turn every entry of a setting into another kind of setting, keeping which
|
|
490
|
+
* modifier each belongs to.
|
|
491
|
+
*
|
|
492
|
+
* A drag sensitivity is a number and a keyboard amount is a tuple, but the two
|
|
493
|
+
* describe the same thing from the caller's side. This carries one over to the
|
|
494
|
+
* other so that a component can hand a sensitivity to {@link applyDelta}
|
|
495
|
+
* without unpicking the modifier map itself — which matters, since naming a
|
|
496
|
+
* modifier is also what takes `step` out of the pipeline.
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* mapModifier({ default: 1, shift: 0.1 }, (f) => ['raw', step * f])
|
|
500
|
+
* // { default: ['raw', 1], shift: ['raw', 0.1] }
|
|
501
|
+
*/
|
|
502
|
+
function mapModifier(options, fn) {
|
|
503
|
+
if (!isModifierMap(options)) return fn(options);
|
|
504
|
+
const mapped = { default: fn(options.default) };
|
|
505
|
+
for (const modifier of MODIFIER_ORDER) {
|
|
506
|
+
const value = options[modifier];
|
|
507
|
+
if (value !== void 0) mapped[modifier] = fn(value);
|
|
508
|
+
}
|
|
509
|
+
return mapped;
|
|
510
|
+
}
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/input/apply-delta.ts
|
|
513
|
+
/**
|
|
514
|
+
* Move a value by an amount of input, as reported by a wheel or an arrow key.
|
|
515
|
+
*
|
|
516
|
+
* The pipeline matches {@link createDragValue}: scale, then step, then clamp.
|
|
517
|
+
* Which key or which sign of `deltaY` counts as which direction is left to the
|
|
518
|
+
* caller, since it differs per component.
|
|
519
|
+
*
|
|
520
|
+
* @param direction which way, and how many times, to apply the option. The
|
|
521
|
+
* size of one step is `option[1]`, so this is normally `1` or `-1`.
|
|
522
|
+
*
|
|
523
|
+
* @param modifiers the event, for `options` that name a modifier key. See
|
|
524
|
+
* {@link selectModifier}.
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* // ArrowDown on a slider whose keyboard option is ['raw', 1]
|
|
528
|
+
* applyDelta(value, -1, keyboard, { min, max, step, scale })
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* // Shift+ArrowDown, where `keyboard` is { default: …, shift: ['raw', 0.1] }
|
|
532
|
+
* applyDelta(value, -1, keyboard, range, event)
|
|
533
|
+
*/
|
|
534
|
+
function applyDelta(value, direction, options, { min, max, step, scale = linearScale }, modifiers) {
|
|
535
|
+
if (min >= max) throw new RangeError("requirements: min < max");
|
|
536
|
+
if (step !== void 0 && (!Number.isFinite(step) || step <= 0)) throw new RangeError("applyDelta step: requirements: finite and greater than 0");
|
|
537
|
+
const { value: [mode, amount], modifier } = selectModifier(options, modifiers);
|
|
538
|
+
const x = direction * amount;
|
|
539
|
+
const next = mode === "normalized" ? scale.denormalize(scale.normalize(value, min, max) + x, min, max) : value + x;
|
|
540
|
+
const quantum = modifier === null ? step : void 0;
|
|
541
|
+
const stepped = quantum !== void 0 ? stepValue(next, quantum) : next;
|
|
542
|
+
return clamp(toPrecision(stepped), min, max);
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
545
|
+
//#region src/selection/box.ts
|
|
546
|
+
function rectOf(from, to) {
|
|
547
|
+
return {
|
|
548
|
+
x: Math.min(from[0], to[0]),
|
|
549
|
+
y: Math.min(from[1], to[1]),
|
|
550
|
+
width: Math.abs(to[0] - from[0]),
|
|
551
|
+
height: Math.abs(to[1] - from[1])
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/** Whether the box covers the point, edges included. */
|
|
555
|
+
function selectionBoxCovers(box, [x, y]) {
|
|
556
|
+
return x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Selecting by dragging a box over a set of items.
|
|
560
|
+
*
|
|
561
|
+
* The drag itself is not here: the caller owns the pointer, and reports where
|
|
562
|
+
* it went in the same 0..1 space the items are given in.
|
|
563
|
+
*/
|
|
564
|
+
function createSelectionBox(options) {
|
|
565
|
+
let opts = options;
|
|
566
|
+
let drag = null;
|
|
567
|
+
let box = null;
|
|
568
|
+
function setBox(next) {
|
|
569
|
+
box = next;
|
|
570
|
+
opts.onBoxChange?.(next);
|
|
571
|
+
}
|
|
572
|
+
function apply(rect, base) {
|
|
573
|
+
const inside = [];
|
|
574
|
+
for (const [id, at] of opts.items()) if (selectionBoxCovers(rect, at) && !base.includes(id)) inside.push(id);
|
|
575
|
+
opts.onSelectionChange?.([...base, ...inside]);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
update: (next) => {
|
|
579
|
+
opts = {
|
|
580
|
+
...opts,
|
|
581
|
+
...next
|
|
582
|
+
};
|
|
583
|
+
},
|
|
584
|
+
begin: (at, { additive = false, selection = [] } = {}) => {
|
|
585
|
+
drag = {
|
|
586
|
+
from: at,
|
|
587
|
+
base: additive ? [...selection] : []
|
|
588
|
+
};
|
|
589
|
+
setBox(rectOf(at, at));
|
|
590
|
+
if (!additive) opts.onSelectionChange?.([]);
|
|
591
|
+
},
|
|
592
|
+
move: (to) => {
|
|
593
|
+
if (!drag) return;
|
|
594
|
+
const rect = rectOf(drag.from, to);
|
|
595
|
+
setBox(rect);
|
|
596
|
+
apply(rect, drag.base);
|
|
597
|
+
},
|
|
598
|
+
end: () => {
|
|
599
|
+
const running = drag !== null;
|
|
600
|
+
drag = null;
|
|
601
|
+
if (box !== null) setBox(null);
|
|
602
|
+
return running;
|
|
603
|
+
},
|
|
604
|
+
box: () => box,
|
|
605
|
+
destroy: () => {
|
|
606
|
+
drag = null;
|
|
607
|
+
box = null;
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/points-editor/index.ts
|
|
613
|
+
/**
|
|
614
|
+
* A point's value is its position in the editor, so its range is the editor:
|
|
615
|
+
* no scaling, and no rounding to a step.
|
|
616
|
+
*/
|
|
617
|
+
const POINT_AXIS = {
|
|
618
|
+
min: 0,
|
|
619
|
+
max: 1
|
|
620
|
+
};
|
|
621
|
+
/**
|
|
622
|
+
* How much one wheel notch moves a point by default: a hundredth of the
|
|
623
|
+
* editor, whatever its pixel size.
|
|
624
|
+
*/
|
|
625
|
+
const POINTS_EDITOR_DEFAULT_WHEEL = ["normalized", .01];
|
|
626
|
+
/**
|
|
627
|
+
* How much one arrow key press moves a point by default. Shift is the
|
|
628
|
+
* fine-adjustment key everywhere else, so it is bound here too — but only on
|
|
629
|
+
* the keyboard. On the wheel it already means the x axis, and browsers hand
|
|
630
|
+
* shift+wheel over as horizontal scrolling anyway.
|
|
631
|
+
*/
|
|
632
|
+
const POINTS_EDITOR_DEFAULT_KEYBOARD = {
|
|
633
|
+
default: ["normalized", .01],
|
|
634
|
+
shift: ["normalized", .001]
|
|
635
|
+
};
|
|
636
|
+
/** Keep a point within its range. An axis left out runs from 0 to 1. */
|
|
637
|
+
function clampPoint(point, min, max) {
|
|
638
|
+
return {
|
|
639
|
+
x: clamp(point.x, min?.x ?? 0, max?.x ?? 1),
|
|
640
|
+
y: clamp(point.y, min?.y ?? 0, max?.y ?? 1)
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const EMPTY = [];
|
|
644
|
+
/**
|
|
645
|
+
* How far a group may move before something in it leaves its range.
|
|
646
|
+
*
|
|
647
|
+
* Clamping each point on its own would break the shape of the selection: the
|
|
648
|
+
* one that reached the edge would stop while the rest carried on. One amount
|
|
649
|
+
* for all of them means the whole selection stops together.
|
|
650
|
+
*/
|
|
651
|
+
function allowedDelta(delta, entries) {
|
|
652
|
+
let loX = -Infinity;
|
|
653
|
+
let hiX = Infinity;
|
|
654
|
+
let loY = -Infinity;
|
|
655
|
+
let hiY = Infinity;
|
|
656
|
+
for (const { start, point } of entries) {
|
|
657
|
+
if (point.readonly) continue;
|
|
658
|
+
loX = Math.max(loX, (point.min?.x ?? 0) - start.x);
|
|
659
|
+
hiX = Math.min(hiX, (point.max?.x ?? 1) - start.x);
|
|
660
|
+
loY = Math.max(loY, (point.min?.y ?? 0) - start.y);
|
|
661
|
+
hiY = Math.min(hiY, (point.max?.y ?? 1) - start.y);
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
x: hiX < loX ? 0 : clamp(delta.x, loX, hiX),
|
|
665
|
+
y: hiY < loY ? 0 : clamp(delta.y, loY, hiY)
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* The selection and the moves of a points editor: which points a press or a
|
|
670
|
+
* box selects, and how a selection moves as one.
|
|
671
|
+
*
|
|
672
|
+
* The points stay with the wrapper, which registers each one; the editor only
|
|
673
|
+
* reads them when it needs to, so a point's value can change on every frame
|
|
674
|
+
* of a drag without anything being re-registered.
|
|
675
|
+
*/
|
|
676
|
+
function createPointsEditor(options = {}) {
|
|
677
|
+
let opts = options;
|
|
678
|
+
const points = /* @__PURE__ */ new Map();
|
|
679
|
+
/** What the current drag picked up, and where those points started. */
|
|
680
|
+
let dragged = [];
|
|
681
|
+
const selection = () => opts.selectable ? opts.selection ?? EMPTY : EMPTY;
|
|
682
|
+
function changeSelection(next) {
|
|
683
|
+
opts = {
|
|
684
|
+
...opts,
|
|
685
|
+
selection: next
|
|
686
|
+
};
|
|
687
|
+
opts.onSelectionChange?.(next);
|
|
688
|
+
}
|
|
689
|
+
function snapshot(ids) {
|
|
690
|
+
return ids.flatMap((id) => {
|
|
691
|
+
const point = points.get(id)?.();
|
|
692
|
+
return point ? [{
|
|
693
|
+
id,
|
|
694
|
+
start: { ...point.value }
|
|
695
|
+
}] : [];
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function moveFrom(entries, delta) {
|
|
699
|
+
const withPoint = entries.flatMap((entry) => {
|
|
700
|
+
const point = points.get(entry.id)?.();
|
|
701
|
+
return point ? [{
|
|
702
|
+
...entry,
|
|
703
|
+
point
|
|
704
|
+
}] : [];
|
|
705
|
+
});
|
|
706
|
+
const allowed = allowedDelta(delta, withPoint);
|
|
707
|
+
for (const { start, point } of withPoint) {
|
|
708
|
+
if (point.readonly) continue;
|
|
709
|
+
point.onChange?.({
|
|
710
|
+
x: toPrecision(start.x + allowed.x),
|
|
711
|
+
y: toPrecision(start.y + allowed.y)
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function nudgeSelection(id, delta) {
|
|
716
|
+
const current = selection();
|
|
717
|
+
moveFrom(snapshot(current.includes(id) ? current : [id]), delta);
|
|
718
|
+
}
|
|
719
|
+
function nudgePoint(id, axis, direction, option, modifiers) {
|
|
720
|
+
const point = points.get(id)?.();
|
|
721
|
+
if (!point) return;
|
|
722
|
+
const { value } = point;
|
|
723
|
+
const next = applyDelta(value[axis], direction, option, POINT_AXIS, modifiers);
|
|
724
|
+
nudgeSelection(id, {
|
|
725
|
+
x: axis === "x" ? next - value.x : 0,
|
|
726
|
+
y: axis === "y" ? next - value.y : 0
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
const box = createSelectionBox({
|
|
730
|
+
*items() {
|
|
731
|
+
for (const [id, read] of points) {
|
|
732
|
+
const { x, y } = read().value;
|
|
733
|
+
yield [id, [x, y]];
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
onBoxChange: (rect) => opts.onSelectionBoxChange?.(rect),
|
|
737
|
+
onSelectionChange: changeSelection
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
update: (next) => {
|
|
741
|
+
opts = {
|
|
742
|
+
...opts,
|
|
743
|
+
...next
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
registerPoint: (id, read) => {
|
|
747
|
+
points.set(id, read);
|
|
748
|
+
return () => {
|
|
749
|
+
if (points.get(id) === read) points.delete(id);
|
|
750
|
+
};
|
|
751
|
+
},
|
|
752
|
+
isPointElement: (element) => {
|
|
753
|
+
if (!element) return false;
|
|
754
|
+
for (const read of points.values()) if (read().element?.contains(element)) return true;
|
|
755
|
+
return false;
|
|
756
|
+
},
|
|
757
|
+
beginPointDrag: (id, modifiers) => {
|
|
758
|
+
if (!opts.selectable) {
|
|
759
|
+
dragged = snapshot([id]);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const current = selection();
|
|
763
|
+
const additive = modifiers.ctrlKey || modifiers.metaKey;
|
|
764
|
+
let next;
|
|
765
|
+
if (additive) next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];
|
|
766
|
+
else if (current.includes(id)) next = current;
|
|
767
|
+
else next = [id];
|
|
768
|
+
changeSelection([...next]);
|
|
769
|
+
dragged = next.includes(id) ? snapshot(next) : [];
|
|
770
|
+
},
|
|
771
|
+
movePointDrag: (delta) => moveFrom(dragged, delta),
|
|
772
|
+
nudgeSelection,
|
|
773
|
+
nudgePoint,
|
|
774
|
+
nudgeFocusedPoint: (axis, direction, modifiers) => {
|
|
775
|
+
for (const [id, read] of points) {
|
|
776
|
+
const { element, wheel, readonly, onChange, beforeWheel } = read();
|
|
777
|
+
const active = element?.ownerDocument.activeElement;
|
|
778
|
+
if (!active || !element?.contains(active)) continue;
|
|
779
|
+
if (!wheel || readonly || !onChange) return false;
|
|
780
|
+
beforeWheel?.();
|
|
781
|
+
nudgePoint(id, axis, direction, wheel, modifiers);
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
return false;
|
|
785
|
+
},
|
|
786
|
+
beginSelectionBox: (at, modifiers) => {
|
|
787
|
+
if (!opts.selectable) return;
|
|
788
|
+
box.begin([at.x, at.y], {
|
|
789
|
+
additive: modifiers.ctrlKey || modifiers.metaKey,
|
|
790
|
+
selection: selection()
|
|
791
|
+
});
|
|
792
|
+
},
|
|
793
|
+
moveSelectionBox: (to) => box.move([to.x, to.y]),
|
|
794
|
+
endSelectionBox: () => {
|
|
795
|
+
if (!box.end()) return;
|
|
796
|
+
const [first] = selection();
|
|
797
|
+
const element = first ? points.get(first)?.().element : null;
|
|
798
|
+
if (element && "focus" in element) element.focus();
|
|
799
|
+
},
|
|
800
|
+
destroy: () => {
|
|
801
|
+
box.destroy();
|
|
802
|
+
dragged = [];
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
//#endregion
|
|
807
|
+
//#region src/xy.ts
|
|
808
|
+
function isPair(value) {
|
|
809
|
+
return Array.isArray(value);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Spread a setting that may have been given as a single value.
|
|
813
|
+
*
|
|
814
|
+
* The parameter is written out rather than taken as `XYInput<T>`: a
|
|
815
|
+
* conditional type cannot be narrowed, so the constraint stays where it is
|
|
816
|
+
* declared and this takes both forms.
|
|
817
|
+
*
|
|
818
|
+
* The pair is copied rather than passed along, so that the result is a tuple
|
|
819
|
+
* the caller owns even when a readonly one was given.
|
|
820
|
+
*/
|
|
821
|
+
function toXY(value) {
|
|
822
|
+
return isPair(value) ? [value[0], value[1]] : [value, value];
|
|
823
|
+
}
|
|
824
|
+
//#endregion
|
|
825
|
+
export { DEFAULT_DRAG_SENSITIVITY as _, clampPoint as a, matchesAccept as b, mapModifier as c, blackKeyWidth as d, fitWhiteKeyWidth as f, pianoWidth as g, notePosition as h, POINT_AXIS as i, selectModifier as l, noteAt as m, POINTS_EDITOR_DEFAULT_KEYBOARD as n, createPointsEditor as o, getNoteRangeArray as p, POINTS_EDITOR_DEFAULT_WHEEL as r, applyDelta as s, toXY as t, createDrag as u, DEFAULT_KEYBOARD_OPTIONS as v, partitionByAccept as x, DEFAULT_WHEEL_OPTIONS as y };
|
|
826
|
+
|
|
827
|
+
//# sourceMappingURL=xy-lNFl1CTO.js.map
|