@tremolo-ui/functions 0.6.0 → 0.8.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 +7 -248
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -201
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +32 -201
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -235
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -25
- package/src/math.ts +1 -12
- package/src/midi.ts +10 -1
- package/src/scales.ts +1 -62
- package/src/modifiers.ts +0 -141
- package/src/piano.ts +0 -177
- package/src/util.ts +0 -7
package/src/piano.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { isBlackKey, isWhiteKey, noteKey, noteKeys, type NoteKey } from './midi'
|
|
2
|
-
|
|
3
|
-
export type NoteRange = {
|
|
4
|
-
first: number
|
|
5
|
-
last: number
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* `[noteRange.first, noteRange.first + 1, ..., noteRange.last]`
|
|
10
|
-
*/
|
|
11
|
-
export function getNoteRangeArray(noteRange: NoteRange): number[] {
|
|
12
|
-
return Array.from(
|
|
13
|
-
{ length: noteRange.last - noteRange.first + 1 },
|
|
14
|
-
(_, i) => i + noteRange.first,
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The geometry of a drawn keyboard.
|
|
20
|
-
*
|
|
21
|
-
* One description is shared by the drawing and the hit testing, so a key cannot
|
|
22
|
-
* be drawn somewhere other than where it responds.
|
|
23
|
-
*/
|
|
24
|
-
export interface PianoLayout {
|
|
25
|
-
noteRange: NoteRange
|
|
26
|
-
|
|
27
|
-
/** Width of a white key, excluding {@link PianoLayout.keyGap}. */
|
|
28
|
-
whiteKeyWidth: number
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Space between two white keys. Part of the slot a white key occupies, so it
|
|
32
|
-
* still belongs to one of the keys for the purpose of hit testing.
|
|
33
|
-
*
|
|
34
|
-
* @default 1
|
|
35
|
-
*/
|
|
36
|
-
keyGap?: number
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Width of a black key, as a fraction of {@link PianoLayout.whiteKeyWidth}.
|
|
40
|
-
*
|
|
41
|
-
* @default 0.65
|
|
42
|
-
*/
|
|
43
|
-
blackKeyWidthRatio?: number
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Height of a black key, as a fraction of the height of the keyboard.
|
|
47
|
-
*
|
|
48
|
-
* @default 0.6
|
|
49
|
-
*/
|
|
50
|
-
blackKeyHeightRatio?: number
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const DEFAULT_KEY_GAP = 1
|
|
54
|
-
const DEFAULT_BLACK_KEY_WIDTH_RATIO = 0.65
|
|
55
|
-
const DEFAULT_BLACK_KEY_HEIGHT_RATIO = 0.6
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* How many white keys sit at or before each pitch class, counting from C.
|
|
59
|
-
*
|
|
60
|
-
* A black key shares the number of the white key to its left plus one, which
|
|
61
|
-
* puts it on the boundary between the two; {@link notePosition} then shifts it
|
|
62
|
-
* back by half its width to centre it there.
|
|
63
|
-
*/
|
|
64
|
-
const whiteKeysBefore: Record<NoteKey, number> = {
|
|
65
|
-
C: 0,
|
|
66
|
-
'C#': 1,
|
|
67
|
-
D: 1,
|
|
68
|
-
'D#': 2,
|
|
69
|
-
E: 2,
|
|
70
|
-
F: 3,
|
|
71
|
-
'F#': 4,
|
|
72
|
-
G: 4,
|
|
73
|
-
'G#': 5,
|
|
74
|
-
A: 5,
|
|
75
|
-
'A#': 6,
|
|
76
|
-
B: 6,
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Width of a black key in pixels. */
|
|
80
|
-
export function blackKeyWidth(layout: PianoLayout): number {
|
|
81
|
-
return (
|
|
82
|
-
layout.whiteKeyWidth *
|
|
83
|
-
(layout.blackKeyWidthRatio ?? DEFAULT_BLACK_KEY_WIDTH_RATIO)
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function rawNotePosition(note: number, layout: PianoLayout): number {
|
|
88
|
-
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)
|
|
89
|
-
const target = noteKey(note)
|
|
90
|
-
const first = noteKey(layout.noteRange.first)
|
|
91
|
-
|
|
92
|
-
const octave = Math.floor((note - layout.noteRange.first) / 12)
|
|
93
|
-
const octaveOffset =
|
|
94
|
-
noteKeys.indexOf(first) > noteKeys.indexOf(target) ? 1 : 0
|
|
95
|
-
const whiteKeysIn =
|
|
96
|
-
whiteKeysBefore[target] -
|
|
97
|
-
whiteKeysBefore[first] +
|
|
98
|
-
(octave + octaveOffset) * 7
|
|
99
|
-
|
|
100
|
-
return isBlackKey(note)
|
|
101
|
-
? whiteKeysIn * slot - blackKeyWidth(layout) / 2
|
|
102
|
-
: whiteKeysIn * slot
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function pianoBounds(layout: PianoLayout) {
|
|
106
|
-
const notes = getNoteRangeArray(layout.noteRange)
|
|
107
|
-
if (notes.length === 0) return { left: 0, right: 0 }
|
|
108
|
-
|
|
109
|
-
let left = Infinity
|
|
110
|
-
let right = -Infinity
|
|
111
|
-
const whiteWidth = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)
|
|
112
|
-
for (const note of notes) {
|
|
113
|
-
const noteLeft = rawNotePosition(note, layout)
|
|
114
|
-
const width = isBlackKey(note) ? blackKeyWidth(layout) : whiteWidth
|
|
115
|
-
left = Math.min(left, noteLeft)
|
|
116
|
-
right = Math.max(right, noteLeft + width)
|
|
117
|
-
}
|
|
118
|
-
return { left, right }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Width of the whole keyboard in pixels. */
|
|
122
|
-
export function pianoWidth(layout: PianoLayout): number {
|
|
123
|
-
const { left, right } = pianoBounds(layout)
|
|
124
|
-
return right - left
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Offset of the left edge of a key from the left edge of the keyboard, in
|
|
129
|
-
* pixels.
|
|
130
|
-
*
|
|
131
|
-
* Notes outside `noteRange` are placed too, so the value is negative below
|
|
132
|
-
* `noteRange.first`.
|
|
133
|
-
*/
|
|
134
|
-
export function notePosition(note: number, layout: PianoLayout): number {
|
|
135
|
-
return rawNotePosition(note, layout) - pianoBounds(layout).left
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* The note drawn at a point, or null where there is none.
|
|
140
|
-
*
|
|
141
|
-
* Black keys are tested first, so they win where they overlap a white one. A
|
|
142
|
-
* white key covers its gap as well as its width, so the whole width of the
|
|
143
|
-
* keyboard belongs to some key and a click cannot fall between two.
|
|
144
|
-
*
|
|
145
|
-
* @param x offset from the left edge of the keyboard, in pixels
|
|
146
|
-
* @param y offset from its top edge, in pixels
|
|
147
|
-
* @param height height of the keyboard, in pixels
|
|
148
|
-
*/
|
|
149
|
-
export function noteAt(
|
|
150
|
-
x: number,
|
|
151
|
-
y: number,
|
|
152
|
-
height: number,
|
|
153
|
-
layout: PianoLayout,
|
|
154
|
-
): number | null {
|
|
155
|
-
if (x < 0 || x >= pianoWidth(layout) || y < 0 || y >= height) return null
|
|
156
|
-
|
|
157
|
-
const notes = getNoteRangeArray(layout.noteRange)
|
|
158
|
-
const blackHeight =
|
|
159
|
-
height * (layout.blackKeyHeightRatio ?? DEFAULT_BLACK_KEY_HEIGHT_RATIO)
|
|
160
|
-
|
|
161
|
-
if (y < blackHeight) {
|
|
162
|
-
for (const note of notes) {
|
|
163
|
-
if (isWhiteKey(note)) continue
|
|
164
|
-
const left = notePosition(note, layout)
|
|
165
|
-
if (left <= x && x < left + blackKeyWidth(layout)) return note
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)
|
|
170
|
-
for (const note of notes) {
|
|
171
|
-
if (isBlackKey(note)) continue
|
|
172
|
-
const left = notePosition(note, layout)
|
|
173
|
-
if (left <= x && x < left + slot) return note
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return null
|
|
177
|
-
}
|