@tremolo-ui/dom 0.4.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 +934 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +580 -31
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +580 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +923 -90
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/src/canvas/animation.ts +322 -0
- package/src/canvas/context.ts +108 -0
- package/src/index.ts +48 -1
- package/src/midi/access.ts +83 -13
- package/src/midi/input.ts +97 -26
- package/src/midi/message.ts +37 -8
- package/src/piano/index.ts +205 -0
- package/src/pointer/drag-value.ts +365 -0
- package/src/pointer/drag.ts +340 -65
- package/src/pointer/wheel.ts +36 -1
- package/src/selection/box.ts +136 -0
- package/src/xy.ts +38 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { XY } from '../xy'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A rectangle in the 0..1 space of whatever the box is drawn over, with `y`
|
|
5
|
+
* growing downwards — the same space the items are given in.
|
|
6
|
+
*/
|
|
7
|
+
export interface SelectionBoxRect {
|
|
8
|
+
x: number
|
|
9
|
+
y: number
|
|
10
|
+
width: number
|
|
11
|
+
height: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SelectionBoxOptions<Id> {
|
|
15
|
+
/**
|
|
16
|
+
* Everything the box can pick up, read on every move rather than taken as a
|
|
17
|
+
* snapshot: what is under the box changes while it is dragged, and an item
|
|
18
|
+
* may have moved since the last one.
|
|
19
|
+
*/
|
|
20
|
+
items: () => Iterable<readonly [Id, XY<number>]>
|
|
21
|
+
/** The box as it is dragged, and `null` once it is gone. */
|
|
22
|
+
onBoxChange?: (box: SelectionBoxRect | null) => void
|
|
23
|
+
/** What the box covers, added to whatever it started from. */
|
|
24
|
+
onSelectionChange?: (ids: Id[]) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SelectionBoxBeginOptions<Id> {
|
|
28
|
+
/**
|
|
29
|
+
* Add to `selection` rather than replacing it. Ctrl / meta rather than
|
|
30
|
+
* shift, for the controls where shift is the fine-adjustment key.
|
|
31
|
+
*/
|
|
32
|
+
additive?: boolean
|
|
33
|
+
/** What was selected before the box started. Only read when `additive`. */
|
|
34
|
+
selection?: readonly Id[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SelectionBoxInstance<Id> {
|
|
38
|
+
/** Replace the given options, without interrupting a box in progress. */
|
|
39
|
+
update: (options: Partial<SelectionBoxOptions<Id>>) => void
|
|
40
|
+
|
|
41
|
+
/** Start a box at `at`, which is one of its corners. */
|
|
42
|
+
begin: (at: XY<number>, options?: SelectionBoxBeginOptions<Id>) => void
|
|
43
|
+
/** Drag the opposite corner to `to`. */
|
|
44
|
+
move: (to: XY<number>) => void
|
|
45
|
+
/**
|
|
46
|
+
* Finish the box. The selection stays as it is.
|
|
47
|
+
*
|
|
48
|
+
* @returns whether a box was running, so that a caller can tell a drag from
|
|
49
|
+
* a press that selected nothing.
|
|
50
|
+
*/
|
|
51
|
+
end: () => boolean
|
|
52
|
+
|
|
53
|
+
/** The box being dragged, or `null` when there is none. */
|
|
54
|
+
box: () => SelectionBoxRect | null
|
|
55
|
+
|
|
56
|
+
destroy: () => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rectOf(from: XY<number>, to: XY<number>): SelectionBoxRect {
|
|
60
|
+
return {
|
|
61
|
+
x: Math.min(from[0], to[0]),
|
|
62
|
+
y: Math.min(from[1], to[1]),
|
|
63
|
+
width: Math.abs(to[0] - from[0]),
|
|
64
|
+
height: Math.abs(to[1] - from[1]),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether the box covers the point, edges included. */
|
|
69
|
+
export function selectionBoxCovers(
|
|
70
|
+
box: SelectionBoxRect,
|
|
71
|
+
[x, y]: XY<number>,
|
|
72
|
+
): boolean {
|
|
73
|
+
return (
|
|
74
|
+
x >= box.x &&
|
|
75
|
+
x <= box.x + box.width &&
|
|
76
|
+
y >= box.y &&
|
|
77
|
+
y <= box.y + box.height
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Selecting by dragging a box over a set of items.
|
|
83
|
+
*
|
|
84
|
+
* The drag itself is not here: the caller owns the pointer, and reports where
|
|
85
|
+
* it went in the same 0..1 space the items are given in.
|
|
86
|
+
*/
|
|
87
|
+
export function createSelectionBox<Id>(
|
|
88
|
+
options: SelectionBoxOptions<Id>,
|
|
89
|
+
): SelectionBoxInstance<Id> {
|
|
90
|
+
let opts = options
|
|
91
|
+
let drag: { from: XY<number>; base: readonly Id[] } | null = null
|
|
92
|
+
let box: SelectionBoxRect | null = null
|
|
93
|
+
|
|
94
|
+
function setBox(next: SelectionBoxRect | null) {
|
|
95
|
+
box = next
|
|
96
|
+
opts.onBoxChange?.(next)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function apply(rect: SelectionBoxRect, base: readonly Id[]) {
|
|
100
|
+
const inside: Id[] = []
|
|
101
|
+
for (const [id, at] of opts.items()) {
|
|
102
|
+
if (selectionBoxCovers(rect, at) && !base.includes(id)) inside.push(id)
|
|
103
|
+
}
|
|
104
|
+
opts.onSelectionChange?.([...base, ...inside])
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
update: (next) => {
|
|
109
|
+
opts = { ...opts, ...next }
|
|
110
|
+
},
|
|
111
|
+
begin: (at, { additive = false, selection = [] } = {}) => {
|
|
112
|
+
drag = { from: at, base: additive ? [...selection] : [] }
|
|
113
|
+
setBox(rectOf(at, at))
|
|
114
|
+
// A plain press is a deselect of its own, so that a drag that picks
|
|
115
|
+
// nothing up leaves nothing selected.
|
|
116
|
+
if (!additive) opts.onSelectionChange?.([])
|
|
117
|
+
},
|
|
118
|
+
move: (to) => {
|
|
119
|
+
if (!drag) return
|
|
120
|
+
const rect = rectOf(drag.from, to)
|
|
121
|
+
setBox(rect)
|
|
122
|
+
apply(rect, drag.base)
|
|
123
|
+
},
|
|
124
|
+
end: () => {
|
|
125
|
+
const running = drag !== null
|
|
126
|
+
drag = null
|
|
127
|
+
if (box !== null) setBox(null)
|
|
128
|
+
return running
|
|
129
|
+
},
|
|
130
|
+
box: () => box,
|
|
131
|
+
destroy: () => {
|
|
132
|
+
drag = null
|
|
133
|
+
box = null
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/xy.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pair of per-axis values. The tuple elements are labelled, so editors show
|
|
3
|
+
* `[x: number, y: number]` rather than a bare pair.
|
|
4
|
+
*/
|
|
5
|
+
export type XY<T> = [x: T, y: T]
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A setting that may be given once for both axes, or per axis.
|
|
9
|
+
*
|
|
10
|
+
* A single value is told from a pair with `Array.isArray`, so the single form
|
|
11
|
+
* is only offered while `T` cannot itself be an array. Where it can, the pair
|
|
12
|
+
* is the only way to write it, since a lone array would be read as a pair.
|
|
13
|
+
*/
|
|
14
|
+
export type XYInput<T> = [T] extends [readonly unknown[]]
|
|
15
|
+
? readonly [x: T, y: T]
|
|
16
|
+
: T | readonly [x: T, y: T]
|
|
17
|
+
|
|
18
|
+
// `Array.isArray` narrows a mutable tuple on its own, but not a readonly one,
|
|
19
|
+
// hence the explicit predicate.
|
|
20
|
+
function isPair<T>(
|
|
21
|
+
value: T | readonly [x: T, y: T],
|
|
22
|
+
): value is readonly [x: T, y: T] {
|
|
23
|
+
return Array.isArray(value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Spread a setting that may have been given as a single value.
|
|
28
|
+
*
|
|
29
|
+
* The parameter is written out rather than taken as `XYInput<T>`: a
|
|
30
|
+
* conditional type cannot be narrowed, so the constraint stays where it is
|
|
31
|
+
* declared and this takes both forms.
|
|
32
|
+
*
|
|
33
|
+
* The pair is copied rather than passed along, so that the result is a tuple
|
|
34
|
+
* the caller owns even when a readonly one was given.
|
|
35
|
+
*/
|
|
36
|
+
export function toXY<T>(value: T | readonly [x: T, y: T]): XY<T> {
|
|
37
|
+
return isPair(value) ? [value[0], value[1]] : [value, value]
|
|
38
|
+
}
|