vector-score 1.0.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/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/classes/MusicStaff.d.ts +127 -0
- package/dist/classes/NoteRenderer.d.ts +27 -0
- package/dist/classes/RhythmStaff.d.ts +118 -0
- package/dist/classes/SVGRenderer.d.ts +47 -0
- package/dist/classes/ScrollingStaff.d.ts +67 -0
- package/dist/constants.d.ts +19 -0
- package/dist/glyphs.d.ts +7 -0
- package/dist/helpers/notehelpers.d.ts +9 -0
- package/dist/index.d.ts +7 -0
- package/dist/strategies/GrandStaffStrategy.d.ts +14 -0
- package/dist/strategies/SingleStaffStrategy.d.ts +12 -0
- package/dist/strategies/StrategyInterface.d.ts +20 -0
- package/dist/types.d.ts +11 -0
- package/dist/vector-score.js +1224 -0
- package/dist/vector-score.umd.cjs +1 -0
- package/package.json +34 -0
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
const B = {
|
|
2
|
+
treble: {
|
|
3
|
+
staffType: "treble",
|
|
4
|
+
paddingTop: 13,
|
|
5
|
+
paddingBottom: 3,
|
|
6
|
+
topLineNote: { name: "F", octave: 5 },
|
|
7
|
+
topLineYPos: 0,
|
|
8
|
+
bottomLineYPos: 40
|
|
9
|
+
},
|
|
10
|
+
bass: {
|
|
11
|
+
staffType: "bass",
|
|
12
|
+
paddingTop: 0,
|
|
13
|
+
paddingBottom: 0,
|
|
14
|
+
topLineNote: { name: "A", octave: 3 },
|
|
15
|
+
topLineYPos: 0,
|
|
16
|
+
bottomLineYPos: 40
|
|
17
|
+
},
|
|
18
|
+
alto: {
|
|
19
|
+
staffType: "alto",
|
|
20
|
+
paddingTop: 0,
|
|
21
|
+
paddingBottom: 0,
|
|
22
|
+
topLineNote: { name: "G", octave: 4 },
|
|
23
|
+
topLineYPos: 0,
|
|
24
|
+
bottomLineYPos: 40
|
|
25
|
+
},
|
|
26
|
+
grand: {
|
|
27
|
+
staffType: "grand",
|
|
28
|
+
paddingTop: 13,
|
|
29
|
+
paddingBottom: 0,
|
|
30
|
+
topLineNote: { name: "F", octave: 5 },
|
|
31
|
+
topLineYPos: 0,
|
|
32
|
+
bottomLineYPos: 110
|
|
33
|
+
}
|
|
34
|
+
}, d = {
|
|
35
|
+
w: 4,
|
|
36
|
+
h: 2,
|
|
37
|
+
q: 1,
|
|
38
|
+
e: 0.5,
|
|
39
|
+
s: 0.25
|
|
40
|
+
}, X = {
|
|
41
|
+
CLEF_TREBLE: {
|
|
42
|
+
path: "m150 273 10 58c2 7 2 7 12 7 59 0 96 46 96 97 0 45-26 79-67 95-5 2-6 2-5 7 4 25 12 63 12 85 0 68-52 80-79 80-61 0-76-39-76-65 0-25 16-46 43-46 24 0 38 19 38 41 0 23-14 34-27 38-9 2-13 4-13 6 0 6 11 12 32 12 24 0 64-7 64-66 0-19-6-54-11-81-1-5-1-4-6-3l-27 2C48 540 0 474 0 404c0-80 61-138 119-185 5-4 4-5 3-10-2-16-5-42-5-65 0-42 9-92 39-125 8-9 20-19 26-19 4 0 15 11 21 20 16 24 26 58 26 93 0 61-33 112-76 153-3 2-3 2-3 7Zm38-211c-24 0-53 38-53 101l2 37c1 5 3 5 5 3 32-28 70-64 70-108 0-22-11-33-24-33Zm-44 272-8-51c-1-4-2-5-6-1-18 15-37 30-61 56-33 38-37 70-37 93 0 56 45 95 115 95l23-2c6-2 6-2 5-6l-20-119c-1-5-1-5-8-3-24 6-40 24-40 46 0 19 12 36 29 43 3 1 6 3 6 5 0 3-2 5-5 5l-11-3c-27-9-46-34-46-70 0-34 23-66 58-78 8-2 8-2 6-10Zm28 64 20 114c0 5 1 5 6 2 22-11 38-31 38-56 0-36-27-63-60-66-4 0-5 1-4 6Z",
|
|
43
|
+
xOffset: 0,
|
|
44
|
+
yOffset: -14
|
|
45
|
+
},
|
|
46
|
+
CLEF_BASS: {
|
|
47
|
+
path: "M101 0c68 0 110 46 110 114 0 120-102 190-199 237l-7 2c-3 0-5-2-5-5s2-5 6-7c92-52 146-114 146-223 0-62-18-103-60-103-40 0-64 29-64 44 0 4 1 8 7 8 5 0 9-3 19-3 20 0 38 15 38 41 0 24-17 41-42 41-32 0-48-27-48-58C2 50 33 0 101 0Zm148 32c13 0 22 10 22 22s-9 22-22 22c-12 0-21-10-21-22s9-22 21-22Zm1 99c12 0 21 9 21 21s-9 22-21 22-21-10-21-22 9-21 21-21Z",
|
|
48
|
+
xOffset: 0,
|
|
49
|
+
yOffset: 0
|
|
50
|
+
},
|
|
51
|
+
CLEF_ALTO: {
|
|
52
|
+
path: "M91 9v174c0 3 2 2 3 2 11-3 27-13 36-58 1-6 3-10 7-10s6 4 8 11c5 17 15 37 43 37 25 0 32-25 32-77s-9-75-42-75c-5 0-33 2-33 10 0 2 6 5 11 6 7 3 15 11 15 26 0 17-11 27-26 27-17 0-31-11-31-32 0-25 22-50 69-50 65 0 93 45 93 87 0 54-30 92-83 92-11 0-18-2-24-4-4-1-8-1-11 1-6 3-14 16-14 24s8 21 14 24c3 2 7 2 11 1 6-2 13-4 24-4 53 0 83 38 83 92 0 42-28 87-93 87-47 0-69-25-69-50 0-21 14-32 31-32 15 0 26 10 26 27 0 15-8 23-15 26-5 1-11 4-11 6 0 8 28 10 33 10 33 0 42-23 42-75s-7-77-32-77c-28 0-38 20-43 37-2 7-4 11-8 11s-6-4-7-10c-9-45-25-55-36-58-1 0-3-1-3 2v174c0 5-3 8-8 8h-1c-5 0-8-3-8-8V9c0-5 3-8 8-8h1c5 0 8 3 8 8ZM8 1h34c6 0 9 3 9 8v382c0 5-3 8-9 8H8c-5 0-8-3-8-8V9c0-5 3-8 8-8Z",
|
|
53
|
+
xOffset: 0,
|
|
54
|
+
yOffset: 0
|
|
55
|
+
},
|
|
56
|
+
NOTE_HEAD_WHOLE: {
|
|
57
|
+
path: "M77 0c34 0 74 19 74 45 0 24-19 45-77 45C21 90 0 68 0 45 0 20 30 0 77 0Zm-9 8c-17 0-30 5-30 24 0 22 23 50 47 50 16 0 28-8 28-26 0-21-20-48-45-48Z",
|
|
58
|
+
xOffset: 0,
|
|
59
|
+
yOffset: -4.5
|
|
60
|
+
},
|
|
61
|
+
NOTE_HEAD_HALF: {
|
|
62
|
+
path: "M35 90C15 90 0 79 0 60 0 42 17 0 70 0c21 0 36 12 36 30 0 12-12 60-71 60Zm-8-14c18 0 68-31 68-47l-2-6c-3-5-7-8-14-8-17 0-68 29-68 46l2 7c2 4 7 8 14 8Z",
|
|
63
|
+
xOffset: 0,
|
|
64
|
+
yOffset: -4.5
|
|
65
|
+
},
|
|
66
|
+
NOTE_HEAD_QUARTER: {
|
|
67
|
+
path: "M35 90C16 90 0 79 0 60 0 29 32 0 71 0c20 0 35 12 35 30 0 30-40 60-71 60Z",
|
|
68
|
+
xOffset: 0,
|
|
69
|
+
yOffset: -4.5
|
|
70
|
+
},
|
|
71
|
+
EIGHTH_NOTE: {
|
|
72
|
+
path: "M142 89c20 32 36 70 36 110 0 25-8 55-8 55-2 5-5 7-8 6h-1c-2-1-5-4-5-9l1-5c5-14 8-29 8-44 0-19-3-37-8-48-10-23-33-58-53-70v179c0 30-38 60-70 60-19 0-34-11-34-30 0-31 31-60 70-60 11 0 19 3 25 8V3c0-2 2-3 3-3 6 0 9 2 10 7 5 31 18 57 34 82Z",
|
|
73
|
+
xOffset: 0,
|
|
74
|
+
yOffset: -28
|
|
75
|
+
},
|
|
76
|
+
EIGHTH_NOTE_FLIPPED: {
|
|
77
|
+
path: "M9 323H0V60C0 29 32 0 71 0c20 0 35 12 35 30 0 30-40 60-71 60-19 0-26-9-26-9v158s58-56 58-89c0 0 0-25-8-43-5-12 10-17 14-6 13 34 9 48 9 48 0 39-41 97-41 97-20 27-32 77-32 77Z",
|
|
78
|
+
xOffset: 0,
|
|
79
|
+
yOffset: -4
|
|
80
|
+
},
|
|
81
|
+
REST_WHOLE: {
|
|
82
|
+
path: "M0 0h104v53H0V0Z",
|
|
83
|
+
xOffset: 0,
|
|
84
|
+
yOffset: 0
|
|
85
|
+
},
|
|
86
|
+
REST_HALF: {
|
|
87
|
+
path: "M0 0h104v53H0V0Z",
|
|
88
|
+
xOffset: 0,
|
|
89
|
+
yOffset: -5
|
|
90
|
+
},
|
|
91
|
+
REST_QUARTER: {
|
|
92
|
+
path: "m28 151-18-22s-3-4-3-9c0-3 1-7 5-11 16-17 22-33 22-46 0-27-21-45-23-49l-1-6c0-5 4-8 7-8s5 1 7 3l61 71 1 7-1 5c-10 15-23 34-25 55v3c0 20 11 35 25 52 4 4 14 16 14 20 0 2-2 2-5 1-6-2-19-6-29-6-16 0-23 12-23 27 0 8 5 22 11 22 3 3 6 6 6 9s-2 5-7 5l-9-3c-27-21-43-39-43-57s13-35 32-35c3 0 10 3 14 0l-2-6-16-22Z",
|
|
93
|
+
xOffset: 0,
|
|
94
|
+
yOffset: -16
|
|
95
|
+
},
|
|
96
|
+
REST_EIGHTH: {
|
|
97
|
+
path: "M61 35c18 0 41-35 47-32 0 1 5 3 5 8l-5 17S64 189 62 190c-4 4-11 5-16 5-3 0-13 0-13-6 8-30 41-122 42-128 1-5 1-10-1-10-4 0-3 3-19 8C16 71 0 46 0 31 0 14 14 0 31 0c5 0 30 1 30 35Z",
|
|
98
|
+
xOffset: 0,
|
|
99
|
+
yOffset: -10
|
|
100
|
+
},
|
|
101
|
+
ACCIDENTAL_SHARP: {
|
|
102
|
+
path: "m67 66-8 3c-2 0-3 6-3 8v26c0 4 2 5 3 5l8-2 1-1c1 0 2 1 2 3v20c0 1-1 4-3 4l-8 4c-2 0-3 4-3 6v40c0 2-2 4-5 4-2 0-4-2-4-4v-35c0-2-1-5-4-5h-1l-17 7c-1 1-3 3-3 6v40c0 2-1 3-4 3-2 0-4-1-4-3v-35c0-1-1-5-3-5l-1 1-7 2v1c-2 0-3-1-3-3v-20c0-2 1-4 3-5l8-3c2-1 3-3 3-6V94c0-3-2-5-4-5l-7 3c-2 0-3-1-3-3V69l3-4 8-3c1-1 3-4 3-8V16c0-2 2-4 5-4 2 0 3 2 3 4v34c0 2 1 5 4 5l18-7c2-1 3-5 3-8V3c0-2 2-3 5-3 2 0 4 1 4 3v35c0 2 2 3 4 3l7-2h1c1 0 2 0 2 2v20c0 2-1 4-3 5Zm-20 46c2-11 2-23 0-34l-4-2c-7 0-20 6-21 11v35l4 1c6 0 20-5 21-11Z",
|
|
103
|
+
xOffset: -8,
|
|
104
|
+
yOffset: -10
|
|
105
|
+
},
|
|
106
|
+
ACCIDENTAL_FLAT: {
|
|
107
|
+
path: "M4 196C1 193 0 9 0 9c0-6 5-9 10-9 3 0 6 2 6 5l-2 91c0 3 1 5 3 6h2l7-4c5-3 12-6 18-6 15 1 29 13 29 31 0 15-10 35-39 55-8 5-16 14-25 19l-2 1c-1 0-2 0-3-2Zm11-28c0 1 1 5 4 5l3-1c14-9 29-27 29-44 0-8-4-19-14-19-7 0-20 10-22 16l-1 10 1 33Z",
|
|
108
|
+
xOffset: -8,
|
|
109
|
+
yOffset: -14
|
|
110
|
+
},
|
|
111
|
+
ACCIDENTAL_NATURAL: {
|
|
112
|
+
path: "m41 47 5-2h1l2 2v147c0 3-2 4-3 4h-4c-2 0-4-1-4-4v-43c0-2-2-3-5-3-8 0-25 7-29 8l-1 1c-2 0-3-1-3-3V4c0-3 1-4 4-4h3c2 0 4 1 4 4v48c0 2 1 2 3 2 7 0 26-7 26-7h1ZM11 88v31l3 1c8 0 24-6 24-12V78l-2-1c-7 0-25 7-25 11Z",
|
|
113
|
+
xOffset: -8,
|
|
114
|
+
yOffset: -10
|
|
115
|
+
},
|
|
116
|
+
ACCIDENTAL_DOUBLE_SHARP: {
|
|
117
|
+
path: "M68 33h3c6 0 13 0 15-2l2-15C88 4 86 0 72 0c-6 0-12 1-14 4-2 1-2 7-2 13-1 5-8 17-12 17-5 0-9-10-11-15l-1-2c0-6-1-12-3-13-2-3-8-4-13-4C10 0 4 1 2 4L0 17l2 14c2 2 9 2 15 2h4c5 2 16 8 16 12 0 3-13 10-17 12h-3c-6 0-13 1-15 3L0 74l2 14 14 2 13-2c2-2 3-8 3-14 1-5 7-17 12-17 4 0 10 13 12 17 0 6 0 12 2 14l14 2c15 0 16-2 16-17l-2-13c-4-2-11-3-15-3h-3c-5-2-17-7-17-12 0-2 13-10 17-12Z",
|
|
118
|
+
xOffset: -10,
|
|
119
|
+
yOffset: -4
|
|
120
|
+
},
|
|
121
|
+
ACCIDENTAL_DOUBLE_FLAT: {
|
|
122
|
+
path: "M102 93h2c15 0 29 12 29 30 0 15-10 35-39 55-8 5-16 14-26 19l-2 1-2-2-3-43c-6 7-15 16-27 25-7 5-15 14-25 19l-2 1c-1 0-2 0-3-2C2 193 0 8 0 8c0-5 6-8 10-8 3 0 6 2 6 5l-2 91c0 3 1 5 3 6h2c3 0 5-3 8-4 5-3 9-5 15-5h2c6 0 12 1 17 5L60 8c0-5 5-8 10-8 3 0 6 2 6 5l-2 91c0 3 1 5 3 6h2c3 0 5-3 7-4 6-3 10-5 16-5Zm-80 78c14-9 29-25 29-43 0-8-3-19-13-19-8 0-21 10-23 16l-1 11 1 31c0 2 1 5 4 5l3-1Zm59 0c15-9 29-25 29-43 0-6-2-12-5-16-2-2-4-3-8-3-7 0-21 10-23 16v8l1 34c0 2 1 5 3 5l3-1Z",
|
|
123
|
+
xOffset: -14,
|
|
124
|
+
yOffset: -14
|
|
125
|
+
},
|
|
126
|
+
TIME_4: {
|
|
127
|
+
path: "M53 0h66S70 76 20 124h61V86l47-50v89h22v19h-22v27c0 9 13 9 13 9h9v10H61v-10h10s10 0 10-10v-26H0v-20S54 67 53 0Z",
|
|
128
|
+
xOffset: 0,
|
|
129
|
+
yOffset: 0
|
|
130
|
+
},
|
|
131
|
+
TIME_3: {
|
|
132
|
+
path: "M2 43c0 21 15 30 30 30 5 0 28-2 28-27 0-11-16-20-16-24 0-14 56-15 56 26 0 26-19 38-31 38H45v13h24c12 0 33 15 33 41 0 38-54 40-54 25 0-3 10-11 10-21 0-20-15-27-27-27-17 0-32 8-31 30 1 30 36 43 75 43 36 0 75-19 75-50 0-34-23-47-37-47v-2c4 0 35-13 35-39 0-37-35-52-75-52C39 0 3 14 2 43Z",
|
|
133
|
+
xOffset: 0,
|
|
134
|
+
yOffset: 0
|
|
135
|
+
}
|
|
136
|
+
}, x = /^(?<name>[A-Ga-g])(?<accidental>##|bb|[#bn]?)(?<octave>\d)(?<duration>[whqeWHQE]?)$/, U = /^[whqeWHQE]$/, I = ["C", "D", "E", "F", "G", "A", "B"];
|
|
137
|
+
function g(h) {
|
|
138
|
+
const e = h.match(x);
|
|
139
|
+
if (!e || !e.groups)
|
|
140
|
+
throw new Error(`Invalid note string format: ${h}. Expected format: [A-Ga-g][#|b]?[0-9][w|h|q|e].`);
|
|
141
|
+
let { name: t, accidental: s, octave: r, duration: o } = e.groups;
|
|
142
|
+
if (t = t.toUpperCase(), o = o.toLowerCase(), !t)
|
|
143
|
+
throw new Error(`Invalid note name: ${t}. Valid note names are: C, D, E, F, G, A, B.`);
|
|
144
|
+
if (!r)
|
|
145
|
+
throw new Error(`Invalid octave: ${r}. Octave must be a number between 0 and 9.`);
|
|
146
|
+
o || (o = "w");
|
|
147
|
+
const n = {
|
|
148
|
+
name: t,
|
|
149
|
+
octave: parseInt(r),
|
|
150
|
+
duration: o
|
|
151
|
+
};
|
|
152
|
+
return s && (n.accidental = s), n;
|
|
153
|
+
}
|
|
154
|
+
function T(h) {
|
|
155
|
+
const e = h.match(U);
|
|
156
|
+
if (!e) throw new Error(`Invalid note duration '${h}'. Use w | h | q | e.`);
|
|
157
|
+
return e.toString().toLowerCase();
|
|
158
|
+
}
|
|
159
|
+
function S(h) {
|
|
160
|
+
let e;
|
|
161
|
+
switch (h) {
|
|
162
|
+
case "treble":
|
|
163
|
+
e = "CLEF_TREBLE";
|
|
164
|
+
break;
|
|
165
|
+
case "bass":
|
|
166
|
+
e = "CLEF_BASS";
|
|
167
|
+
break;
|
|
168
|
+
case "alto":
|
|
169
|
+
e = "CLEF_ALTO";
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (!e) throw new Error(`Invalid clef type: ${h}. Valid clef types are: treble, bass, alto.`);
|
|
173
|
+
return e;
|
|
174
|
+
}
|
|
175
|
+
function D(h, e) {
|
|
176
|
+
const t = I.indexOf(h.name) - I.indexOf(e.name);
|
|
177
|
+
let s = h.octave - e.octave;
|
|
178
|
+
return s *= 7, t + s;
|
|
179
|
+
}
|
|
180
|
+
function R(h) {
|
|
181
|
+
let e = I.indexOf(h.name);
|
|
182
|
+
return e += h.octave * 12, e;
|
|
183
|
+
}
|
|
184
|
+
function y(h, e) {
|
|
185
|
+
return I.findIndex((s) => s === h) + 1 + e * 7;
|
|
186
|
+
}
|
|
187
|
+
const N = 48, w = 40 + 30 / 2, b = 20, F = 90;
|
|
188
|
+
class H {
|
|
189
|
+
params;
|
|
190
|
+
rendererRef;
|
|
191
|
+
width = 0;
|
|
192
|
+
constructor(e, t) {
|
|
193
|
+
this.rendererRef = e;
|
|
194
|
+
const s = B[t];
|
|
195
|
+
if (!s) throw new Error(`Staff type ${t} is not supported`);
|
|
196
|
+
this.params = s;
|
|
197
|
+
}
|
|
198
|
+
drawStaffLines = (e, t) => {
|
|
199
|
+
let s = e;
|
|
200
|
+
for (let r = 0; r < 5; r++)
|
|
201
|
+
this.rendererRef.drawLine(0, s, this.width, s, t), s += 10;
|
|
202
|
+
return s - 10;
|
|
203
|
+
};
|
|
204
|
+
drawStaff = (e) => {
|
|
205
|
+
this.width = e;
|
|
206
|
+
const t = this.rendererRef.getLayerByName("staff");
|
|
207
|
+
let s = this.drawStaffLines(0, t), r = this.drawStaffLines(s + 30, t);
|
|
208
|
+
this.rendererRef.drawLine(0, 0, 0, r, t), this.rendererRef.drawLine(this.width, 0, this.width, r, t);
|
|
209
|
+
let o = r, n = 1;
|
|
210
|
+
const a = S("treble"), i = S("bass");
|
|
211
|
+
this.rendererRef.drawGlyph(a, t), this.rendererRef.drawGlyph(i, t, { yOffset: s + 30 }), o += this.params.paddingBottom + 1, n += this.params.paddingTop, this.rendererRef.addTotalRootSvgHeight(o), this.rendererRef.addTotalRootSvgYOffset(n);
|
|
212
|
+
};
|
|
213
|
+
shouldNoteFlip(e) {
|
|
214
|
+
const t = Math.abs(e - F), s = Math.abs(e - b);
|
|
215
|
+
return e === w ? !1 : t <= s ? !(e >= F) : e <= b;
|
|
216
|
+
}
|
|
217
|
+
getLedgerLinesX(e, t) {
|
|
218
|
+
const s = [], r = R(e);
|
|
219
|
+
let o = e.duration === "w" ? 17 : 12.5;
|
|
220
|
+
if (r === N)
|
|
221
|
+
return s.push({ x1: -2, x2: o, yPos: 0 }), s;
|
|
222
|
+
if (t < this.params.topLineYPos) {
|
|
223
|
+
let n = this.params.topLineYPos - 10;
|
|
224
|
+
for (; n >= t; )
|
|
225
|
+
s.push({
|
|
226
|
+
x1: -2,
|
|
227
|
+
x2: o,
|
|
228
|
+
yPos: n - t
|
|
229
|
+
}), n -= 10;
|
|
230
|
+
} else if (t > this.params.bottomLineYPos) {
|
|
231
|
+
let n = this.params.bottomLineYPos + 10;
|
|
232
|
+
for (; n <= t; )
|
|
233
|
+
s.push({
|
|
234
|
+
x1: -2,
|
|
235
|
+
x2: o,
|
|
236
|
+
yPos: n - t
|
|
237
|
+
}), n += 10;
|
|
238
|
+
}
|
|
239
|
+
return s;
|
|
240
|
+
}
|
|
241
|
+
calculateNoteYPos = (e) => {
|
|
242
|
+
let s = D(this.params.topLineNote, e) * (10 / 2);
|
|
243
|
+
const r = R(e);
|
|
244
|
+
return r < N ? s += 10 : r === N && (s = w), s;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const M = 20;
|
|
248
|
+
class A {
|
|
249
|
+
params;
|
|
250
|
+
rendererRef;
|
|
251
|
+
constructor(e, t) {
|
|
252
|
+
this.rendererRef = e;
|
|
253
|
+
const s = B[t];
|
|
254
|
+
if (!s) throw new Error(`Staff type ${t} is not supported`);
|
|
255
|
+
this.params = s;
|
|
256
|
+
}
|
|
257
|
+
drawStaff = (e) => {
|
|
258
|
+
const t = this.rendererRef.getLayerByName("staff");
|
|
259
|
+
let s = 0;
|
|
260
|
+
for (let a = 0; a < 5; a++)
|
|
261
|
+
this.rendererRef.drawLine(0, s, e, s, t), s += 10;
|
|
262
|
+
this.rendererRef.drawLine(0, 0, 0, s - 10, t), this.rendererRef.drawLine(e, 0, e, s - 10, t);
|
|
263
|
+
let r = s - 10 + 1, o = 1;
|
|
264
|
+
const n = S(this.params.staffType);
|
|
265
|
+
this.rendererRef.drawGlyph(n, t), r += this.params.paddingTop + this.params.paddingBottom, o += this.params.paddingTop, this.rendererRef.addTotalRootSvgHeight(r), this.rendererRef.addTotalRootSvgYOffset(o);
|
|
266
|
+
};
|
|
267
|
+
shouldNoteFlip(e) {
|
|
268
|
+
return e <= M;
|
|
269
|
+
}
|
|
270
|
+
getLedgerLinesX(e, t) {
|
|
271
|
+
const s = [];
|
|
272
|
+
let r = e.duration === "w" ? 17 : 12.5;
|
|
273
|
+
if (t < this.params.topLineYPos) {
|
|
274
|
+
let o = this.params.topLineYPos - 10;
|
|
275
|
+
for (; o >= t; )
|
|
276
|
+
s.push({
|
|
277
|
+
x1: -2,
|
|
278
|
+
x2: r,
|
|
279
|
+
yPos: o - t
|
|
280
|
+
}), o -= 10;
|
|
281
|
+
} else if (t > this.params.bottomLineYPos) {
|
|
282
|
+
let o = this.params.bottomLineYPos + 10;
|
|
283
|
+
for (; o <= t; )
|
|
284
|
+
s.push({
|
|
285
|
+
x1: -2,
|
|
286
|
+
x2: r,
|
|
287
|
+
yPos: o - t
|
|
288
|
+
}), o += 10;
|
|
289
|
+
}
|
|
290
|
+
return s;
|
|
291
|
+
}
|
|
292
|
+
calculateNoteYPos = (e) => D(this.params.topLineNote, e) * (10 / 2);
|
|
293
|
+
}
|
|
294
|
+
class P {
|
|
295
|
+
svgRendererInstance;
|
|
296
|
+
strategyInstance;
|
|
297
|
+
constructor(e, t) {
|
|
298
|
+
this.svgRendererInstance = e, this.strategyInstance = t;
|
|
299
|
+
}
|
|
300
|
+
drawStem(e, t) {
|
|
301
|
+
t ? this.svgRendererInstance.drawLine(0, 0, 0, 28, e) : this.svgRendererInstance.drawLine(10, 0, 10, -28, e);
|
|
302
|
+
}
|
|
303
|
+
chordOffsetConsecutiveAccidentals(e) {
|
|
304
|
+
let t = 0, s = 0, r = 0;
|
|
305
|
+
for (let o = 0; o < e.length; o++) {
|
|
306
|
+
const n = e[o];
|
|
307
|
+
if (n.noteObj.accidental && r < 3 ? (t += -8, s = Math.min(s, t), r++) : n.noteObj.accidental && r <= 3 ? (t = -8, r = 1) : (t = 0, r = 0), t !== 0) {
|
|
308
|
+
const i = Array.from(n.noteGroup.getElementsByTagName("use")).find((c) => c.getAttribute("href")?.includes("ACCIDENTAL"));
|
|
309
|
+
if (!i) continue;
|
|
310
|
+
i.setAttribute("transform", `translate(${t + 8}, 0)`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return -s;
|
|
314
|
+
}
|
|
315
|
+
chordOffsetCloseNotes(e) {
|
|
316
|
+
let t = e[0], s = 0;
|
|
317
|
+
for (let r = 1; r < e.length; r++) {
|
|
318
|
+
const o = e[r];
|
|
319
|
+
if (y(o.noteObj.name, o.noteObj.octave) - y(t.noteObj.name, t.noteObj.octave) === 1) {
|
|
320
|
+
s = 28 / 2, o.noteGroup.setAttribute("transform", `translate(${s}, ${o.noteYPos})`);
|
|
321
|
+
const i = Array.from(o.noteGroup.getElementsByTagName("use")).find((c) => c.getAttribute("href")?.includes("ACCIDENTAL"));
|
|
322
|
+
if (i) {
|
|
323
|
+
const c = i.getAttribute("transform")?.match(/([-]?\d+)/), l = c && c[0];
|
|
324
|
+
let f = -s;
|
|
325
|
+
l && (f += Number(l)), i.setAttribute("transform", `translate(${f}, 0)`);
|
|
326
|
+
}
|
|
327
|
+
r++, t = e[r];
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
t = o;
|
|
331
|
+
}
|
|
332
|
+
return s;
|
|
333
|
+
}
|
|
334
|
+
// Handles drawing the glyphs to internal group, applies the xPositioning to note Cursor X
|
|
335
|
+
renderNote(e) {
|
|
336
|
+
const t = this.svgRendererInstance.createGroup("note"), s = g(e), r = this.strategyInstance.calculateNoteYPos({
|
|
337
|
+
name: s.name,
|
|
338
|
+
octave: s.octave
|
|
339
|
+
});
|
|
340
|
+
let o = this.strategyInstance.shouldNoteFlip(r);
|
|
341
|
+
switch (s.duration) {
|
|
342
|
+
case "h":
|
|
343
|
+
this.svgRendererInstance.drawGlyph("NOTE_HEAD_HALF", t), this.drawStem(t, o);
|
|
344
|
+
break;
|
|
345
|
+
case "q":
|
|
346
|
+
this.svgRendererInstance.drawGlyph("NOTE_HEAD_QUARTER", t), this.drawStem(t, o);
|
|
347
|
+
break;
|
|
348
|
+
case "e":
|
|
349
|
+
o ? this.svgRendererInstance.drawGlyph("EIGHTH_NOTE_FLIPPED", t) : this.svgRendererInstance.drawGlyph("EIGHTH_NOTE", t);
|
|
350
|
+
break;
|
|
351
|
+
default:
|
|
352
|
+
this.svgRendererInstance.drawGlyph("NOTE_HEAD_WHOLE", t);
|
|
353
|
+
}
|
|
354
|
+
let n = 0;
|
|
355
|
+
switch (s.accidental) {
|
|
356
|
+
case "#":
|
|
357
|
+
this.svgRendererInstance.drawGlyph("ACCIDENTAL_SHARP", t), n -= -8;
|
|
358
|
+
break;
|
|
359
|
+
case "b":
|
|
360
|
+
this.svgRendererInstance.drawGlyph("ACCIDENTAL_FLAT", t), n -= -8;
|
|
361
|
+
break;
|
|
362
|
+
case "n":
|
|
363
|
+
this.svgRendererInstance.drawGlyph("ACCIDENTAL_NATURAL", t), n -= -8;
|
|
364
|
+
break;
|
|
365
|
+
case "##":
|
|
366
|
+
this.svgRendererInstance.drawGlyph("ACCIDENTAL_DOUBLE_SHARP", t), n -= -10;
|
|
367
|
+
break;
|
|
368
|
+
case "bb":
|
|
369
|
+
this.svgRendererInstance.drawGlyph("ACCIDENTAL_DOUBLE_FLAT", t), n -= -14;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
return this.strategyInstance.getLedgerLinesX(s, r).forEach((i) => {
|
|
373
|
+
this.svgRendererInstance.drawLine(i.x1, i.yPos, i.x2, i.yPos, t);
|
|
374
|
+
}), {
|
|
375
|
+
noteGroup: t,
|
|
376
|
+
noteObj: s,
|
|
377
|
+
noteYPos: r,
|
|
378
|
+
accidentalOffset: n,
|
|
379
|
+
cursorOffset: 0
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
renderChord(e) {
|
|
383
|
+
const t = this.svgRendererInstance.createGroup("chord"), s = [];
|
|
384
|
+
for (const n of e) {
|
|
385
|
+
const a = this.renderNote(n);
|
|
386
|
+
a.noteGroup.setAttribute("transform", `translate(0, ${a.noteYPos})`), t.appendChild(a.noteGroup), s.push({
|
|
387
|
+
noteGroup: a.noteGroup,
|
|
388
|
+
noteObj: a.noteObj,
|
|
389
|
+
noteYPos: a.noteYPos,
|
|
390
|
+
cursorOffset: 0,
|
|
391
|
+
accidentalOffset: 0
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const r = this.chordOffsetConsecutiveAccidentals(s), o = this.chordOffsetCloseNotes(s);
|
|
395
|
+
return {
|
|
396
|
+
noteGroup: t,
|
|
397
|
+
noteObj: s[0].noteObj,
|
|
398
|
+
noteYPos: 0,
|
|
399
|
+
accidentalOffset: r,
|
|
400
|
+
cursorOffset: o
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const u = "http://www.w3.org/2000/svg", Y = 0.1, m = 1200;
|
|
405
|
+
class O {
|
|
406
|
+
rootElementRef;
|
|
407
|
+
svgElementRef;
|
|
408
|
+
// Layers
|
|
409
|
+
parentGroupContainer;
|
|
410
|
+
musicStaffLayer;
|
|
411
|
+
musicNotesLayer;
|
|
412
|
+
musicUILayer;
|
|
413
|
+
// Positioning variables
|
|
414
|
+
width;
|
|
415
|
+
scale;
|
|
416
|
+
totalYOffset = 0;
|
|
417
|
+
totalHeight = 0;
|
|
418
|
+
// Setups root SVG element and layers, sets attributes for scaling, creates defs for glyphs
|
|
419
|
+
// Does not append to DOM automatically, must be done manually with commitElementsToDOM and passing rootSvgElement
|
|
420
|
+
// from get rootSvgElement().
|
|
421
|
+
// HEIGHT and YOfsset should be set externally, values are calculated internally.
|
|
422
|
+
constructor(e, t) {
|
|
423
|
+
this.rootElementRef = e, this.width = t.width, this.scale = t.scale, this.width > m && (this.width = m, console.warn(`Width provided for the staff ${this.width} exceeds the limit ${m}. Please use this value to fix positioning issues.`)), this.svgElementRef = document.createElementNS(u, "svg"), this.svgElementRef.classList.add("vs-svg-renderer-root"), this.svgElementRef.style.maxWidth = "100%", this.svgElementRef.style.height = "auto", this.svgElementRef.style.display = "block", this.svgElementRef.setAttribute("color", t.staffColor), this.svgElementRef.style.backgroundColor = t.staffBackgroundColor, this.makeGlyphDefs(t.useGlyphs), this.parentGroupContainer = this.createGroup("svg-renderer-parent"), this.svgElementRef.appendChild(this.parentGroupContainer), this.musicStaffLayer = this.createGroup("music-staff-layer"), this.musicNotesLayer = this.createGroup("music-notes-layer"), this.musicUILayer = this.createGroup("music-ui-layer"), this.parentGroupContainer.appendChild(this.musicStaffLayer), this.parentGroupContainer.appendChild(this.musicNotesLayer), this.parentGroupContainer.appendChild(this.musicUILayer), this.musicNotesLayer.setAttribute("transform", "translate(38, 0)");
|
|
424
|
+
}
|
|
425
|
+
// Creates SVG defs for all glyphs in GLYPH_ENTRIES, applies global scale and offsets, appends to root SVG
|
|
426
|
+
makeGlyphDefs(e) {
|
|
427
|
+
const t = document.createElementNS(u, "defs");
|
|
428
|
+
Object.entries(X).filter(([r]) => e.includes(r)).forEach(([r, o]) => {
|
|
429
|
+
const n = document.createElementNS(u, "path");
|
|
430
|
+
n.setAttribute("id", `glyph-${r}`), n.setAttribute("d", o.path), n.setAttribute("fill", "currentColor"), n.setAttribute("transform", `translate(${o.xOffset}, ${o.yOffset}) scale(${Y})`), t.appendChild(n);
|
|
431
|
+
}), this.rootSvgElement.appendChild(t);
|
|
432
|
+
}
|
|
433
|
+
createGroup(e) {
|
|
434
|
+
const t = document.createElementNS(u, "g");
|
|
435
|
+
return e && t.classList.add(`vs-${e}`), t;
|
|
436
|
+
}
|
|
437
|
+
commitElementsToDOM(e, t = this.rootElementRef) {
|
|
438
|
+
const s = document.createDocumentFragment();
|
|
439
|
+
Array.isArray(e) ? e.forEach((r) => {
|
|
440
|
+
s.appendChild(r);
|
|
441
|
+
}) : s.appendChild(e), t.appendChild(s);
|
|
442
|
+
}
|
|
443
|
+
getLayerByName(e) {
|
|
444
|
+
switch (e) {
|
|
445
|
+
case "staff":
|
|
446
|
+
return this.musicStaffLayer;
|
|
447
|
+
case "notes":
|
|
448
|
+
return this.musicNotesLayer;
|
|
449
|
+
case "ui":
|
|
450
|
+
return this.musicUILayer;
|
|
451
|
+
default:
|
|
452
|
+
throw new Error(`Layer with name '${e}' does not exist.`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
addTotalRootSvgHeight(e) {
|
|
456
|
+
this.totalHeight += e;
|
|
457
|
+
}
|
|
458
|
+
addTotalRootSvgYOffset(e) {
|
|
459
|
+
this.totalYOffset += e;
|
|
460
|
+
}
|
|
461
|
+
applySizingToRootSvg() {
|
|
462
|
+
this.parentGroupContainer.setAttribute("transform", `translate(0, ${this.totalYOffset})`);
|
|
463
|
+
let e = this.width * this.scale;
|
|
464
|
+
const t = (this.totalHeight + this.totalYOffset) * this.scale;
|
|
465
|
+
e += 2, this.svgElementRef.setAttribute("width", e.toString()), this.svgElementRef.setAttribute("height", t.toString()), this.svgElementRef.setAttribute("viewBox", `0 0 ${this.width} ${this.totalHeight + this.totalYOffset}`);
|
|
466
|
+
}
|
|
467
|
+
get rootSvgElement() {
|
|
468
|
+
return this.svgElementRef;
|
|
469
|
+
}
|
|
470
|
+
get parentGroupElement() {
|
|
471
|
+
return this.parentGroupContainer;
|
|
472
|
+
}
|
|
473
|
+
// Drawing Methods
|
|
474
|
+
drawLine(e, t, s, r, o) {
|
|
475
|
+
const n = document.createElementNS(u, "line");
|
|
476
|
+
n.setAttribute("x1", e.toString()), n.setAttribute("y1", t.toString()), n.setAttribute("x2", s.toString()), n.setAttribute("y2", r.toString()), n.setAttribute("stroke", "currentColor"), n.setAttribute("stroke-width", "1"), o.appendChild(n);
|
|
477
|
+
}
|
|
478
|
+
drawRect(e, t, s, r) {
|
|
479
|
+
const o = document.createElementNS(u, "rect");
|
|
480
|
+
return o.setAttribute("width", e.toString()), o.setAttribute("height", t.toString()), o.setAttribute("x", (r?.x ?? 0).toString()), o.setAttribute("y", (r?.y ?? 0).toString()), r?.fill ? o.setAttribute("fill", r.fill) : o.setAttribute("fill", "currentColor"), s.appendChild(o), o;
|
|
481
|
+
}
|
|
482
|
+
drawGlyph(e, t, s) {
|
|
483
|
+
s = {
|
|
484
|
+
xOffset: 0,
|
|
485
|
+
yOffset: 0,
|
|
486
|
+
...s
|
|
487
|
+
};
|
|
488
|
+
const r = document.createElementNS(u, "use");
|
|
489
|
+
r.setAttribute("href", `#glyph-${e}`), (s.xOffset || s.yOffset) && r.setAttribute("transform", `translate(${s.xOffset}, ${s.yOffset})`), t.appendChild(r);
|
|
490
|
+
}
|
|
491
|
+
destroy() {
|
|
492
|
+
this.rootElementRef && this.svgElementRef && this.rootElementRef.contains(this.svgElementRef) && this.rootElementRef.removeChild(this.svgElementRef);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const $ = [
|
|
496
|
+
"CLEF_TREBLE",
|
|
497
|
+
"CLEF_BASS",
|
|
498
|
+
"CLEF_ALTO",
|
|
499
|
+
"NOTE_HEAD_WHOLE",
|
|
500
|
+
"NOTE_HEAD_HALF",
|
|
501
|
+
"NOTE_HEAD_QUARTER",
|
|
502
|
+
"EIGHTH_NOTE",
|
|
503
|
+
"EIGHTH_NOTE_FLIPPED",
|
|
504
|
+
"ACCIDENTAL_SHARP",
|
|
505
|
+
"ACCIDENTAL_FLAT",
|
|
506
|
+
"ACCIDENTAL_NATURAL",
|
|
507
|
+
"ACCIDENTAL_DOUBLE_SHARP",
|
|
508
|
+
"ACCIDENTAL_DOUBLE_FLAT"
|
|
509
|
+
];
|
|
510
|
+
class j {
|
|
511
|
+
svgRendererInstance;
|
|
512
|
+
strategyInstance;
|
|
513
|
+
noteRendererInstance;
|
|
514
|
+
options;
|
|
515
|
+
noteEntries = [];
|
|
516
|
+
noteCursorX = 0;
|
|
517
|
+
/**
|
|
518
|
+
* Creates an instance of a MusicStaff, A single staff.
|
|
519
|
+
*
|
|
520
|
+
* @param rootElementCtx - The element (div) reference that will append the music staff elements to.
|
|
521
|
+
* @param options - Optional configuration settings. Can adjust staff type (treble, bass, alto, grand), width, scale, spaces above/below, etc. All config options are in the type MusicStaffOptions
|
|
522
|
+
*/
|
|
523
|
+
constructor(e, t) {
|
|
524
|
+
this.options = {
|
|
525
|
+
width: 300,
|
|
526
|
+
scale: 1,
|
|
527
|
+
staffType: "treble",
|
|
528
|
+
spaceAbove: 0,
|
|
529
|
+
spaceBelow: 0,
|
|
530
|
+
staffColor: "black",
|
|
531
|
+
staffBackgroundColor: "transparent",
|
|
532
|
+
...t
|
|
533
|
+
}, this.svgRendererInstance = new O(e, {
|
|
534
|
+
width: this.options.width,
|
|
535
|
+
height: 100,
|
|
536
|
+
scale: this.options.scale,
|
|
537
|
+
staffColor: this.options.staffColor,
|
|
538
|
+
staffBackgroundColor: this.options.staffBackgroundColor,
|
|
539
|
+
useGlyphs: $
|
|
540
|
+
});
|
|
541
|
+
const s = this.svgRendererInstance.rootSvgElement;
|
|
542
|
+
switch (this.options.staffType) {
|
|
543
|
+
case "grand":
|
|
544
|
+
this.strategyInstance = new H(this.svgRendererInstance, "grand");
|
|
545
|
+
break;
|
|
546
|
+
case "bass":
|
|
547
|
+
this.strategyInstance = new A(this.svgRendererInstance, "bass");
|
|
548
|
+
break;
|
|
549
|
+
case "treble":
|
|
550
|
+
this.strategyInstance = new A(this.svgRendererInstance, "treble");
|
|
551
|
+
break;
|
|
552
|
+
case "alto":
|
|
553
|
+
this.strategyInstance = new A(this.svgRendererInstance, "alto");
|
|
554
|
+
break;
|
|
555
|
+
default:
|
|
556
|
+
throw new Error(`The staff type ${this.options.staffType} is not supported. Please use "treble", "bass", "alto", or "grand".`);
|
|
557
|
+
}
|
|
558
|
+
if (this.strategyInstance.drawStaff(this.options.width), this.noteRendererInstance = new P(this.svgRendererInstance, this.strategyInstance), this.options.spaceAbove) {
|
|
559
|
+
const r = this.options.spaceAbove * 10;
|
|
560
|
+
this.svgRendererInstance.addTotalRootSvgYOffset(r);
|
|
561
|
+
}
|
|
562
|
+
if (this.options.spaceBelow) {
|
|
563
|
+
let r = this.options.spaceBelow * 10;
|
|
564
|
+
this.options.staffType === "grand" && (r -= 10 / 2), this.svgRendererInstance.addTotalRootSvgHeight(r);
|
|
565
|
+
}
|
|
566
|
+
this.svgRendererInstance.applySizingToRootSvg(), this.svgRendererInstance.commitElementsToDOM(s);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Draws a note on the staff.
|
|
570
|
+
* @param notes - A single string OR array of note strings in the format `[Root][Accidental?][Octave][Duration?]`.
|
|
571
|
+
* If an array is passed, it will draw each individual note on the staff.
|
|
572
|
+
*
|
|
573
|
+
* * **Root**: (A-G)
|
|
574
|
+
* * **Accidental** (Optional): `#` (sharp) `b` (flat) `n` (natural) `##` (double sharp) or `bb` (double flat).
|
|
575
|
+
* * **Octave**: The octave number (e.g., `4`).
|
|
576
|
+
* * **Duration** (Optional): `w` (whole) `h` (half) `q` (quarter) or `e` (eighth). Defaults to `w` if duration is omitted
|
|
577
|
+
* @returns void
|
|
578
|
+
* @throws {Error} If a note string is not correct format. If an array was passed, it will still draw whatever correctly formatted notes before it.
|
|
579
|
+
*
|
|
580
|
+
* * @example
|
|
581
|
+
* // Draws the specified notes individually on the staff
|
|
582
|
+
* drawNote(["D4w", "F4w", "A4w", "B#5q", "Ebb4e"]);
|
|
583
|
+
*
|
|
584
|
+
* * @example
|
|
585
|
+
* // Draws the specified single note on the staff
|
|
586
|
+
* drawNote("D4w");
|
|
587
|
+
*/
|
|
588
|
+
drawNote(e) {
|
|
589
|
+
const t = Array.isArray(e) ? e : [e], s = this.svgRendererInstance.getLayerByName("notes"), r = [];
|
|
590
|
+
for (const o of t) {
|
|
591
|
+
let n;
|
|
592
|
+
try {
|
|
593
|
+
n = this.noteRendererInstance.renderNote(o);
|
|
594
|
+
} catch (a) {
|
|
595
|
+
throw r.length > 0 && this.svgRendererInstance.commitElementsToDOM(r, s), a;
|
|
596
|
+
}
|
|
597
|
+
n.noteGroup.setAttribute("transform", `translate(${this.noteCursorX + n.accidentalOffset}, ${n.noteYPos})`), this.noteEntries.push({
|
|
598
|
+
gElement: n.noteGroup,
|
|
599
|
+
note: n.noteObj,
|
|
600
|
+
xPos: this.noteCursorX + n.accidentalOffset,
|
|
601
|
+
yPos: n.noteYPos,
|
|
602
|
+
accidentalXOffset: n.accidentalOffset
|
|
603
|
+
}), this.noteCursorX += 28 + n.accidentalOffset, r.push(n.noteGroup);
|
|
604
|
+
}
|
|
605
|
+
this.svgRendererInstance.commitElementsToDOM(r, s);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Draws a chord on the staff.
|
|
609
|
+
* @param notes - An array of note strings in the format `[Root][Accidental?][Octave][Duration?]`.
|
|
610
|
+
*
|
|
611
|
+
* * **Root**: (A-G)
|
|
612
|
+
* * **Accidental** (Optional): `#` (sharp) `b` (flat) `n` (natural) `##` (double sharp) or `bb` (double flat).
|
|
613
|
+
* * **Octave**: The octave number (e.g., `4`).
|
|
614
|
+
* * **Duration** (Optional): `w` (whole) `h` (half) `q` (quarter) or `e` (eighth). Defaults to `w` if duration is omitted
|
|
615
|
+
* @returns void
|
|
616
|
+
* @throws {Error} If a note string is not correct format OR if less than one note was provided.
|
|
617
|
+
*
|
|
618
|
+
* * @example
|
|
619
|
+
* // Draw a D minor chord starting on 4th octave
|
|
620
|
+
* drawChord(["D4w", "F4w", "A4w"], 0);
|
|
621
|
+
*/
|
|
622
|
+
drawChord(e) {
|
|
623
|
+
if (e.length < 2) throw new Error("Provide more than one note for a chord.");
|
|
624
|
+
const t = this.svgRendererInstance.getLayerByName("notes"), s = this.noteRendererInstance.renderChord(e);
|
|
625
|
+
s.noteGroup.setAttribute("transform", `translate(${this.noteCursorX + s.accidentalOffset}, 0)`), this.noteEntries.push({
|
|
626
|
+
gElement: s.noteGroup,
|
|
627
|
+
note: g(e[0]),
|
|
628
|
+
xPos: this.noteCursorX + s.accidentalOffset,
|
|
629
|
+
yPos: 0,
|
|
630
|
+
accidentalXOffset: s.accidentalOffset
|
|
631
|
+
}), this.noteCursorX += 28 + s.accidentalOffset + s.cursorOffset, this.svgRendererInstance.commitElementsToDOM(s.noteGroup, t);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Evenly spaces out the notes on the staff.
|
|
635
|
+
* @returns Returns early if no notes are on the staff
|
|
636
|
+
*/
|
|
637
|
+
justifyNotes() {
|
|
638
|
+
const e = this.options.width - 38, t = this.noteEntries.length;
|
|
639
|
+
if (t <= 0 || e <= 0) return;
|
|
640
|
+
const s = Math.round(e / t);
|
|
641
|
+
this.noteEntries.map((o, n) => {
|
|
642
|
+
const a = (n + 0.5) * s, i = o.gElement.getBBox(), c = a - i.width / 2 - i.x, l = Math.round(c * 10) / 10;
|
|
643
|
+
return {
|
|
644
|
+
entry: o,
|
|
645
|
+
newX: l,
|
|
646
|
+
isChord: o.gElement.classList.contains("chord")
|
|
647
|
+
};
|
|
648
|
+
}).forEach((o) => {
|
|
649
|
+
const { entry: n, newX: a, isChord: i } = o;
|
|
650
|
+
i ? n.gElement.setAttribute("transform", `translate(${a}, 0)`) : n.gElement.setAttribute("transform", `translate(${a}, ${n.yPos})`), n.xPos = a;
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Clears staff of notes and resets internal positioning.
|
|
655
|
+
* @returns void
|
|
656
|
+
*/
|
|
657
|
+
clearAllNotes() {
|
|
658
|
+
this.noteCursorX = 0, this.svgRendererInstance.getLayerByName("notes").replaceChildren(), this.noteEntries = [];
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Changes the note by index to the specified note.
|
|
662
|
+
* @param notes - A note string in the format `[Root][Accidental?][Octave][Duration?]`.
|
|
663
|
+
*
|
|
664
|
+
* * **Root**: (A-G)
|
|
665
|
+
* * **Accidental** (Optional): `#` (sharp) `b` (flat) `n` (natural) `##` (double sharp) or `bb` (double flat).
|
|
666
|
+
* * **Octave**: The octave number (e.g., `4`).
|
|
667
|
+
* * **Duration** (Optional): `w` (whole) `h` (half) `q` (quarter) or `e` (eighth). Defaults to `w` if duration is omitted
|
|
668
|
+
* @param noteIndex The index of the note that will replaced by the specified note.
|
|
669
|
+
* @returns void
|
|
670
|
+
* @throws {Error} If the index provided is out of bounds, or if a note string is not correct format.
|
|
671
|
+
*
|
|
672
|
+
* * @example
|
|
673
|
+
* // Changes note at pos `0` to a B flat quarter note on the 3rd octave
|
|
674
|
+
* changeNoteByIndex("Bb3q", 0);
|
|
675
|
+
*/
|
|
676
|
+
changeNoteByIndex(e, t) {
|
|
677
|
+
if (t >= this.noteEntries.length) throw new Error("Note index was out of bounds.");
|
|
678
|
+
const s = this.noteEntries[t], r = this.noteRendererInstance.renderNote(e), n = s.xPos - s.accidentalXOffset + r.accidentalOffset;
|
|
679
|
+
r.noteGroup.setAttribute("transform", `translate(${n}, ${r.noteYPos})`), this.svgRendererInstance.getLayerByName("notes").replaceChild(r.noteGroup, s.gElement), this.noteEntries[t] = {
|
|
680
|
+
gElement: r.noteGroup,
|
|
681
|
+
note: r.noteObj,
|
|
682
|
+
xPos: n,
|
|
683
|
+
yPos: r.noteYPos,
|
|
684
|
+
accidentalXOffset: r.accidentalOffset
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Changes the note by index to the specified chord.
|
|
689
|
+
* @param notes - An array of note strings in the format `[Root][Accidental?][Octave][Duration?]`.
|
|
690
|
+
*
|
|
691
|
+
* * **Root**: (A-G)
|
|
692
|
+
* * **Accidental** (Optional): `#` (sharp) `b` (flat) `n` (natural) `##` (double sharp) or `bb` (double flat).
|
|
693
|
+
* * **Octave**: The octave number (e.g., `4`).
|
|
694
|
+
* * **Duration** (Optional): `w` (whole) `h` (half) `q` (quarter) or `e` (eighth). Defaults to `w` if duration is omitted
|
|
695
|
+
* @param noteIndex The index of the note that will replaced by the specified chord.
|
|
696
|
+
* @returns void
|
|
697
|
+
* @throws {Error} If the index provided is out of bounds, or if a note string is not correct format.
|
|
698
|
+
*
|
|
699
|
+
* * @example
|
|
700
|
+
* // Changes chord at pos `0` to a C Minor chord
|
|
701
|
+
* changeChordByIndex(["C4w", "D#4w", "G4w"], 0);
|
|
702
|
+
*/
|
|
703
|
+
changeChordByIndex(e, t) {
|
|
704
|
+
if (t >= this.noteEntries.length) throw new Error("Chord index was out of bounds.");
|
|
705
|
+
if (e.length < 2) throw new Error("Notes provided need to be more than one to be considered a chord.");
|
|
706
|
+
const s = this.noteEntries[t], r = this.noteRendererInstance.renderChord(e), n = s.xPos - s.accidentalXOffset + r.accidentalOffset;
|
|
707
|
+
r.noteGroup.setAttribute("transform", `translate(${n}, 0)`), this.svgRendererInstance.getLayerByName("notes").replaceChild(r.noteGroup, s.gElement), this.noteEntries[t] = {
|
|
708
|
+
gElement: r.noteGroup,
|
|
709
|
+
note: g(e[0]),
|
|
710
|
+
xPos: n,
|
|
711
|
+
yPos: 0,
|
|
712
|
+
accidentalXOffset: r.accidentalOffset
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Adds a class to the note by the index provided.
|
|
717
|
+
* @param className The name added to the note
|
|
718
|
+
* @param noteIndex The index of the note that will have 'className' added to it.
|
|
719
|
+
* @returns void
|
|
720
|
+
* @throws {Error} If the index provided is out of bounds
|
|
721
|
+
*/
|
|
722
|
+
addClassToNoteByIndex(e, t) {
|
|
723
|
+
if (t >= this.noteEntries.length) throw new Error("Note index was out of bounds.");
|
|
724
|
+
this.noteEntries[t].gElement.classList.add(e);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Removes a class to the note by the index provided.
|
|
728
|
+
* @param className The name removed from the note
|
|
729
|
+
* @param noteIndex The index of the note that will have 'className' removed from it.
|
|
730
|
+
* @returns void
|
|
731
|
+
* @throws {Error} If the index provided is out of bounds
|
|
732
|
+
*/
|
|
733
|
+
removeClassToNoteByIndex(e, t) {
|
|
734
|
+
if (t >= this.noteEntries.length) throw new Error("Note index was out of bounds.");
|
|
735
|
+
this.noteEntries[t].gElement.classList.remove(e);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Removes the root svg element and cleans up arrays.
|
|
739
|
+
* @returns void
|
|
740
|
+
*/
|
|
741
|
+
destroy() {
|
|
742
|
+
this.noteEntries = [], this.svgRendererInstance.destroy();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const E = 30, G = 19, C = 12, k = 4, W = 6, v = 1, L = 38, Z = [
|
|
746
|
+
"TIME_4",
|
|
747
|
+
"TIME_3",
|
|
748
|
+
"NOTE_HEAD_WHOLE",
|
|
749
|
+
"NOTE_HEAD_HALF",
|
|
750
|
+
"NOTE_HEAD_QUARTER",
|
|
751
|
+
"EIGHTH_NOTE",
|
|
752
|
+
"REST_WHOLE",
|
|
753
|
+
"REST_HALF",
|
|
754
|
+
"REST_QUARTER",
|
|
755
|
+
"REST_EIGHTH"
|
|
756
|
+
];
|
|
757
|
+
class Q {
|
|
758
|
+
rendererInstance;
|
|
759
|
+
options;
|
|
760
|
+
barSpacing;
|
|
761
|
+
quarterNoteSpacing;
|
|
762
|
+
noteCursorX = 0;
|
|
763
|
+
noteEntries = [];
|
|
764
|
+
maxBeatCount;
|
|
765
|
+
currentBeatCount = 0;
|
|
766
|
+
currentBeatUICount = 0;
|
|
767
|
+
currentBeatUIElement = null;
|
|
768
|
+
currentBeatUIXPos = L;
|
|
769
|
+
/**
|
|
770
|
+
* Creates an instance of a RhythmStaff, A single staff that will automatically apply positioning of elements based on the duration of a note.
|
|
771
|
+
*
|
|
772
|
+
* @param rootElementCtx - The element (div) reference that will append the music staff elements to.
|
|
773
|
+
* @param options - Optional configuration settings. All config options are in the type RhythmStaffOptions
|
|
774
|
+
* @throws {Error} - If top number is not 3 or 4 OR if bars count is not between 1 - 3. These are the currently only supported values.
|
|
775
|
+
*/
|
|
776
|
+
constructor(e, t) {
|
|
777
|
+
this.options = {
|
|
778
|
+
width: 300,
|
|
779
|
+
scale: 1,
|
|
780
|
+
topNumber: 4,
|
|
781
|
+
barsCount: 2,
|
|
782
|
+
spaceAbove: 0,
|
|
783
|
+
spaceBelow: 0,
|
|
784
|
+
staffColor: "black",
|
|
785
|
+
staffBackgroundColor: "white",
|
|
786
|
+
currentBeatUIColor: "#24ff7450",
|
|
787
|
+
...t
|
|
788
|
+
}, this.rendererInstance = new O(e, {
|
|
789
|
+
width: this.options.width,
|
|
790
|
+
height: 100,
|
|
791
|
+
scale: this.options.scale,
|
|
792
|
+
staffColor: this.options.staffColor,
|
|
793
|
+
staffBackgroundColor: this.options.staffBackgroundColor,
|
|
794
|
+
useGlyphs: Z
|
|
795
|
+
});
|
|
796
|
+
const s = this.rendererInstance.rootSvgElement;
|
|
797
|
+
let r = "TIME_4";
|
|
798
|
+
switch (this.options.topNumber) {
|
|
799
|
+
case 3:
|
|
800
|
+
r = "TIME_3";
|
|
801
|
+
break;
|
|
802
|
+
case 4:
|
|
803
|
+
r = "TIME_4";
|
|
804
|
+
break;
|
|
805
|
+
default:
|
|
806
|
+
throw new Error(`Time signature ${this.options.topNumber} not supported. Please use either 3 or 4.`);
|
|
807
|
+
}
|
|
808
|
+
if (this.options.barsCount < 1 || this.options.barsCount > 3) throw new Error(`Bars count ${this.options.barsCount} not supported. Please use 1 - 3`);
|
|
809
|
+
if (this.options.spaceAbove) {
|
|
810
|
+
const p = this.options.spaceAbove * 10;
|
|
811
|
+
this.rendererInstance.addTotalRootSvgYOffset(p);
|
|
812
|
+
}
|
|
813
|
+
if (this.options.spaceBelow) {
|
|
814
|
+
let p = this.options.spaceBelow * 10;
|
|
815
|
+
this.rendererInstance.addTotalRootSvgHeight(p);
|
|
816
|
+
}
|
|
817
|
+
const o = this.rendererInstance.getLayerByName("staff");
|
|
818
|
+
this.rendererInstance.addTotalRootSvgHeight(E * 2);
|
|
819
|
+
const n = this.rendererInstance.createGroup("time-signature");
|
|
820
|
+
o.appendChild(n);
|
|
821
|
+
const a = E - G;
|
|
822
|
+
this.rendererInstance.drawGlyph(r, n), this.rendererInstance.drawGlyph("TIME_4", n, { yOffset: G }), n.setAttribute("transform", `translate(0, ${a})`);
|
|
823
|
+
let i = this.options.width - 38;
|
|
824
|
+
this.options.barsCount > 1 && (i -= (this.options.barsCount - 1) * C), i -= v, this.rendererInstance.drawLine(0, E, this.options.width - v, E, o), this.barSpacing = i / this.options.barsCount, this.quarterNoteSpacing = Math.round(this.barSpacing / this.options.topNumber), this.maxBeatCount = this.options.barsCount * this.options.topNumber;
|
|
825
|
+
let c = this.barSpacing + 38;
|
|
826
|
+
const l = E / 2, f = E + l;
|
|
827
|
+
for (let p = 0; p < this.options.barsCount - 1; p++)
|
|
828
|
+
this.rendererInstance.drawLine(c, l, c, f, o), c += this.barSpacing;
|
|
829
|
+
this.rendererInstance.getLayerByName("notes").setAttribute("transform", `translate(38, ${E})`), this.rendererInstance.applySizingToRootSvg(), this.rendererInstance.commitElementsToDOM(s);
|
|
830
|
+
}
|
|
831
|
+
createBeatUIElement() {
|
|
832
|
+
const e = this.rendererInstance.getLayerByName("ui");
|
|
833
|
+
this.currentBeatUIElement = this.rendererInstance.drawRect(
|
|
834
|
+
this.quarterNoteSpacing / 2,
|
|
835
|
+
E * 2,
|
|
836
|
+
e,
|
|
837
|
+
{
|
|
838
|
+
x: L,
|
|
839
|
+
fill: this.options.currentBeatUIColor
|
|
840
|
+
}
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
handleNewBar() {
|
|
844
|
+
this.noteCursorX += C;
|
|
845
|
+
}
|
|
846
|
+
// Translates group, returns cursor increment amount
|
|
847
|
+
translateGroupByDuration(e, t) {
|
|
848
|
+
return t.setAttribute("transform", `translate(${this.noteCursorX}, 0)`), this.quarterNoteSpacing * e;
|
|
849
|
+
}
|
|
850
|
+
drawStem(e, t) {
|
|
851
|
+
this.rendererInstance.drawLine(10 + (t ?? 0), 0, 10 + (t ?? 0), -28, e);
|
|
852
|
+
}
|
|
853
|
+
renderNote(e, t) {
|
|
854
|
+
switch (e) {
|
|
855
|
+
case "w":
|
|
856
|
+
this.rendererInstance.drawGlyph("NOTE_HEAD_WHOLE", t);
|
|
857
|
+
break;
|
|
858
|
+
case "h":
|
|
859
|
+
this.rendererInstance.drawGlyph("NOTE_HEAD_HALF", t), this.drawStem(t);
|
|
860
|
+
break;
|
|
861
|
+
case "q":
|
|
862
|
+
this.rendererInstance.drawGlyph("NOTE_HEAD_QUARTER", t), this.drawStem(t);
|
|
863
|
+
break;
|
|
864
|
+
case "e":
|
|
865
|
+
this.rendererInstance.drawGlyph("EIGHTH_NOTE", t), this.drawStem(t);
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
renderRest(e, t) {
|
|
870
|
+
switch (e) {
|
|
871
|
+
case "w":
|
|
872
|
+
this.rendererInstance.drawGlyph("REST_WHOLE", t);
|
|
873
|
+
break;
|
|
874
|
+
case "h":
|
|
875
|
+
this.rendererInstance.drawGlyph("REST_HALF", t);
|
|
876
|
+
break;
|
|
877
|
+
case "q":
|
|
878
|
+
this.rendererInstance.drawGlyph("REST_QUARTER", t);
|
|
879
|
+
break;
|
|
880
|
+
case "e":
|
|
881
|
+
this.rendererInstance.drawGlyph("REST_EIGHTH", t);
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
checkAndCreateNewBar() {
|
|
886
|
+
const e = this.currentBeatCount > 0 && this.currentBeatCount % this.options.topNumber === 0, t = this.currentBeatCount < this.maxBeatCount;
|
|
887
|
+
e && t && this.handleNewBar();
|
|
888
|
+
}
|
|
889
|
+
checkAndFillBarWithRests(e) {
|
|
890
|
+
const t = this.options.topNumber - this.currentBeatCount % this.options.topNumber;
|
|
891
|
+
if (e > t) {
|
|
892
|
+
const s = this.createRemainingRests(t);
|
|
893
|
+
return this.handleNewBar(), s;
|
|
894
|
+
}
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
// If the last beat exceeded the remaining value in bar, fill the space with approiate rests
|
|
898
|
+
createRemainingRests(e) {
|
|
899
|
+
const t = [];
|
|
900
|
+
let s = e;
|
|
901
|
+
for (; s > 0; ) {
|
|
902
|
+
const r = this.rendererInstance.createGroup("rest");
|
|
903
|
+
let o = 0;
|
|
904
|
+
s - d.h >= 0 ? (this.rendererInstance.drawGlyph("REST_HALF", r), o = d.h) : s - d.q >= 0 ? (this.rendererInstance.drawGlyph("REST_QUARTER", r), o = d.q) : (this.rendererInstance.drawGlyph("REST_EIGHTH", r), o = d.e), s -= o, this.currentBeatCount += o, r.setAttribute("transform", `translate(${this.noteCursorX}, 0)`), this.noteCursorX += o * this.quarterNoteSpacing, t.push(r);
|
|
905
|
+
}
|
|
906
|
+
return t;
|
|
907
|
+
}
|
|
908
|
+
renderBeamRect(e, t, s, r) {
|
|
909
|
+
this.rendererInstance.drawRect(
|
|
910
|
+
e - t,
|
|
911
|
+
k,
|
|
912
|
+
s,
|
|
913
|
+
{
|
|
914
|
+
x: 10,
|
|
915
|
+
y: -28 + (r ?? 0),
|
|
916
|
+
fill: this.options.staffColor
|
|
917
|
+
}
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Draws a note duration on the staff.
|
|
922
|
+
* @param notes - A single string OR array of note strings in the format `[Duration]`.
|
|
923
|
+
* If an array is passed, it will draw each individual note duration on the staff.
|
|
924
|
+
* If a duration exceeds the remaining value on the bar, rests will fill the empty space.
|
|
925
|
+
*
|
|
926
|
+
* * **Duration**: `w` (whole) `h` (half) `q` (quarter) `e` (eighth)
|
|
927
|
+
* @returns void
|
|
928
|
+
* @throws {Error} If a note string is not correct format. If an array was passed, it will still draw whatever correctly formatted notes before it.
|
|
929
|
+
*
|
|
930
|
+
* * @example
|
|
931
|
+
* // Draws the specified note durations individually on the staff
|
|
932
|
+
* drawNote(["q", "q", "q", "q", "w"]);
|
|
933
|
+
*
|
|
934
|
+
* * @example
|
|
935
|
+
* // Draws the specified single note duration on the staff
|
|
936
|
+
* drawNote("w");
|
|
937
|
+
*/
|
|
938
|
+
drawNote(e) {
|
|
939
|
+
const t = Array.isArray(e) ? e : [e], s = this.rendererInstance.getLayerByName("notes"), r = [];
|
|
940
|
+
for (const o of t) {
|
|
941
|
+
let n = "w";
|
|
942
|
+
try {
|
|
943
|
+
n = T(o);
|
|
944
|
+
} catch (f) {
|
|
945
|
+
throw r.length > 0 && this.rendererInstance.commitElementsToDOM(r, s), f;
|
|
946
|
+
}
|
|
947
|
+
const a = d[n];
|
|
948
|
+
if (this.currentBeatCount >= this.maxBeatCount)
|
|
949
|
+
throw r.length > 0 && this.rendererInstance.commitElementsToDOM(r, s), new Error("Max beat count reached. Can't add additional notes.");
|
|
950
|
+
this.checkAndCreateNewBar();
|
|
951
|
+
const i = this.checkAndFillBarWithRests(a);
|
|
952
|
+
i && i.forEach((f) => {
|
|
953
|
+
r.push(f), this.noteEntries.push(f);
|
|
954
|
+
});
|
|
955
|
+
const c = this.rendererInstance.createGroup("note"), l = this.translateGroupByDuration(a, c);
|
|
956
|
+
this.noteCursorX += l, this.currentBeatCount += a, this.renderNote(n, c), r.push(c), this.noteEntries.push(c);
|
|
957
|
+
}
|
|
958
|
+
this.rendererInstance.commitElementsToDOM(r, s);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Draws a rest duration on the staff.
|
|
962
|
+
* @param rests - A single string OR array of rest strings in the format `[Duration]`.
|
|
963
|
+
* If an array is passed, it will draw each individual rest duration on the staff.
|
|
964
|
+
* If a duration exceeds the remaining value on the bar, rests will fill the empty space.
|
|
965
|
+
*
|
|
966
|
+
* * **Duration**: `w` (whole) `h` (half) `q` (quarter) `e` (eighth)
|
|
967
|
+
* @returns void
|
|
968
|
+
* @throws {Error} If a rest string is not correct format. If an array was passed, it will still draw whatever correctly formatted rests before it.
|
|
969
|
+
*
|
|
970
|
+
* * @example
|
|
971
|
+
* // Draws the specified rest durations individually on the staff
|
|
972
|
+
* drawRest(["q", "q", "q", "q", "w"]);
|
|
973
|
+
*
|
|
974
|
+
* * @example
|
|
975
|
+
* // Draws the specified single rest duration on the staff
|
|
976
|
+
* drawRest("w");
|
|
977
|
+
*/
|
|
978
|
+
drawRest(e) {
|
|
979
|
+
const t = Array.isArray(e) ? e : [e], s = this.rendererInstance.getLayerByName("notes"), r = [];
|
|
980
|
+
for (const o of t) {
|
|
981
|
+
let n = "w";
|
|
982
|
+
try {
|
|
983
|
+
n = T(o);
|
|
984
|
+
} catch (f) {
|
|
985
|
+
throw r.length > 0 && this.rendererInstance.commitElementsToDOM(r, s), f;
|
|
986
|
+
}
|
|
987
|
+
const a = this.rendererInstance.createGroup("rest"), i = d[n], c = i * this.quarterNoteSpacing;
|
|
988
|
+
if (this.currentBeatCount >= this.maxBeatCount)
|
|
989
|
+
throw r.length > 0 && this.rendererInstance.commitElementsToDOM(r, s), new Error("Max beat count reached. Can't add additional notes.");
|
|
990
|
+
this.checkAndCreateNewBar();
|
|
991
|
+
const l = this.checkAndFillBarWithRests(i);
|
|
992
|
+
l && l.forEach((f) => {
|
|
993
|
+
r.push(f), this.noteEntries.push(f);
|
|
994
|
+
}), this.renderRest(n, a), a.setAttribute("transform", `translate(${this.noteCursorX}, 0)`), this.noteCursorX += c, this.currentBeatCount += i, r.push(a), this.noteEntries.push(a);
|
|
995
|
+
}
|
|
996
|
+
this.rendererInstance.commitElementsToDOM(r, s);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Draws a beamed note of specified duration/count on the staff.
|
|
1000
|
+
* Will stop beam early if bar line is reached / if beat count is over max limit
|
|
1001
|
+
* @param note - A duration string of either 'e' (eighth) or 's' (sixth).
|
|
1002
|
+
* @param noteCount - The amount of notes in the beam
|
|
1003
|
+
*
|
|
1004
|
+
* @returns void
|
|
1005
|
+
* @throws {Error} If a rest string is not correct format. If an array was passed, it will still draw whatever correctly formatted rests before it.
|
|
1006
|
+
*
|
|
1007
|
+
* * @example
|
|
1008
|
+
* // Draws a 4 beamed eighth note
|
|
1009
|
+
* drawBeamedNotes("e", 4);
|
|
1010
|
+
*
|
|
1011
|
+
* * @example
|
|
1012
|
+
* // Draws a 8 beamed sixth note
|
|
1013
|
+
* drawBeamedNotes("s", 8);
|
|
1014
|
+
*/
|
|
1015
|
+
drawBeamedNotes(e, t) {
|
|
1016
|
+
if (t < 2)
|
|
1017
|
+
throw new Error("Must provide a value greater than 2 for beamed note.");
|
|
1018
|
+
if (this.currentBeatCount >= this.maxBeatCount)
|
|
1019
|
+
throw new Error("Max beat count reached. Can't add additional beamed note.");
|
|
1020
|
+
let s = "s";
|
|
1021
|
+
e === "s" ? s = "s" : s = T(e), this.checkAndCreateNewBar();
|
|
1022
|
+
const r = this.rendererInstance.getLayerByName("notes"), o = d[s], n = o * this.quarterNoteSpacing, a = this.options.topNumber - this.currentBeatCount % this.options.topNumber, i = Math.min(t, a / o), c = this.rendererInstance.createGroup("beamed-note");
|
|
1023
|
+
c.setAttribute("transform", `translate(${this.noteCursorX}, 0)`);
|
|
1024
|
+
let l = 0;
|
|
1025
|
+
for (let f = 0; f < i; f++)
|
|
1026
|
+
this.rendererInstance.drawGlyph("NOTE_HEAD_QUARTER", c, { xOffset: l }), this.drawStem(c, l), l += n, this.currentBeatCount += o;
|
|
1027
|
+
this.renderBeamRect(l, n, c), e === "s" && this.renderBeamRect(l, n, c, W), this.noteCursorX += l, this.noteEntries.push(c), this.rendererInstance.commitElementsToDOM(c, r);
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Will increment the UI showing the current beat in quarters. Once exceeded, must be reset with `resetCurrentBeatUI()`
|
|
1031
|
+
* @returns void
|
|
1032
|
+
*/
|
|
1033
|
+
incrementCurrentBeatUI() {
|
|
1034
|
+
if (this.currentBeatUIElement || this.createBeatUIElement(), this.currentBeatUICount >= this.maxBeatCount) {
|
|
1035
|
+
this.currentBeatUIElement.setAttribute("display", "none");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
this.currentBeatUIElement?.getAttribute("display") === "none" && this.currentBeatUIElement.removeAttribute("display"), this.currentBeatUICount++, this.currentBeatUICount > this.options.topNumber && this.currentBeatUICount % this.options.topNumber === 1 && (this.currentBeatUIXPos += C), this.currentBeatUICount > 1 && (this.currentBeatUIXPos += this.quarterNoteSpacing), this.currentBeatUIElement.setAttribute("x", this.currentBeatUIXPos.toString());
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Resets the ui showing the current beat value.
|
|
1042
|
+
* @returns void
|
|
1043
|
+
*/
|
|
1044
|
+
resetCurrentBeatUI() {
|
|
1045
|
+
this.currentBeatUICount = 0, this.currentBeatUIXPos = L, this.currentBeatUIElement && (this.currentBeatUIElement.setAttribute("display", "none"), this.currentBeatUIElement.setAttribute("x", this.currentBeatUIXPos.toString()));
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Clears staff of notes and resets internal positioning.
|
|
1049
|
+
* @returns void
|
|
1050
|
+
*/
|
|
1051
|
+
clearAllNotes() {
|
|
1052
|
+
this.noteCursorX = 0, this.currentBeatCount = 0, this.rendererInstance.getLayerByName("notes").replaceChildren(), this.noteEntries = [];
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Removes the root svg element and cleans up arrays.
|
|
1056
|
+
* @returns void
|
|
1057
|
+
*/
|
|
1058
|
+
destroy() {
|
|
1059
|
+
this.noteEntries = [], this.rendererInstance.destroy();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const V = [
|
|
1063
|
+
"CLEF_TREBLE",
|
|
1064
|
+
"CLEF_BASS",
|
|
1065
|
+
"CLEF_ALTO",
|
|
1066
|
+
"NOTE_HEAD_WHOLE",
|
|
1067
|
+
"NOTE_HEAD_HALF",
|
|
1068
|
+
"NOTE_HEAD_QUARTER",
|
|
1069
|
+
"EIGHTH_NOTE",
|
|
1070
|
+
"EIGHTH_NOTE_FLIPPED",
|
|
1071
|
+
"ACCIDENTAL_SHARP",
|
|
1072
|
+
"ACCIDENTAL_FLAT",
|
|
1073
|
+
"ACCIDENTAL_NATURAL",
|
|
1074
|
+
"ACCIDENTAL_DOUBLE_SHARP",
|
|
1075
|
+
"ACCIDENTAL_DOUBLE_FLAT"
|
|
1076
|
+
], _ = 60, q = _;
|
|
1077
|
+
class z {
|
|
1078
|
+
svgRendererInstance;
|
|
1079
|
+
strategyInstance;
|
|
1080
|
+
noteRendererInstance;
|
|
1081
|
+
options;
|
|
1082
|
+
activeEntries = [];
|
|
1083
|
+
noteBuffer = [];
|
|
1084
|
+
notesLayer;
|
|
1085
|
+
noteCursorX = 0;
|
|
1086
|
+
/**
|
|
1087
|
+
* Creates an instance of a ScrollingStaff, A single staff that takes in a queue of notes that can be advanced with in a 'endless' style of staff.
|
|
1088
|
+
*
|
|
1089
|
+
* @param rootElementCtx - The element (div) reference that will append the music staff elements to.
|
|
1090
|
+
* @param options - Optional configuration settings. All config options are in the type ScrollingStaffOptions
|
|
1091
|
+
*/
|
|
1092
|
+
constructor(e, t) {
|
|
1093
|
+
this.options = {
|
|
1094
|
+
width: 300,
|
|
1095
|
+
scale: 1,
|
|
1096
|
+
staffType: "treble",
|
|
1097
|
+
spaceAbove: 0,
|
|
1098
|
+
spaceBelow: 0,
|
|
1099
|
+
staffColor: "black",
|
|
1100
|
+
staffBackgroundColor: "transparent",
|
|
1101
|
+
...t
|
|
1102
|
+
}, this.svgRendererInstance = new O(e, {
|
|
1103
|
+
width: this.options.width,
|
|
1104
|
+
height: 100,
|
|
1105
|
+
scale: this.options.scale,
|
|
1106
|
+
staffColor: this.options.staffColor,
|
|
1107
|
+
staffBackgroundColor: this.options.staffBackgroundColor,
|
|
1108
|
+
useGlyphs: V
|
|
1109
|
+
});
|
|
1110
|
+
const s = this.svgRendererInstance.rootSvgElement;
|
|
1111
|
+
switch (this.options.staffType) {
|
|
1112
|
+
case "grand":
|
|
1113
|
+
this.strategyInstance = new H(this.svgRendererInstance, "grand");
|
|
1114
|
+
break;
|
|
1115
|
+
case "bass":
|
|
1116
|
+
this.strategyInstance = new A(this.svgRendererInstance, "bass");
|
|
1117
|
+
break;
|
|
1118
|
+
case "treble":
|
|
1119
|
+
this.strategyInstance = new A(this.svgRendererInstance, "treble");
|
|
1120
|
+
break;
|
|
1121
|
+
case "alto":
|
|
1122
|
+
this.strategyInstance = new A(this.svgRendererInstance, "alto");
|
|
1123
|
+
break;
|
|
1124
|
+
default:
|
|
1125
|
+
throw new Error(`The staff type ${this.options.staffType} is not supported. Please use "treble", "bass", "alto", or "grand".`);
|
|
1126
|
+
}
|
|
1127
|
+
if (this.strategyInstance.drawStaff(this.options.width), this.noteRendererInstance = new P(this.svgRendererInstance, this.strategyInstance), this.options.spaceAbove) {
|
|
1128
|
+
const r = this.options.spaceAbove * 10;
|
|
1129
|
+
this.svgRendererInstance.addTotalRootSvgYOffset(r);
|
|
1130
|
+
}
|
|
1131
|
+
if (this.options.spaceBelow) {
|
|
1132
|
+
let r = this.options.spaceBelow * 10;
|
|
1133
|
+
this.options.staffType === "grand" && (r -= 10 / 2), this.svgRendererInstance.addTotalRootSvgHeight(r);
|
|
1134
|
+
}
|
|
1135
|
+
this.notesLayer = this.svgRendererInstance.getLayerByName("notes"), this.notesLayer.classList.add("vs-scrolling-notes-layer"), this.svgRendererInstance.applySizingToRootSvg(), this.svgRendererInstance.commitElementsToDOM(s);
|
|
1136
|
+
}
|
|
1137
|
+
renderFirstNoteGroups() {
|
|
1138
|
+
const e = this.options.width - 38 + q;
|
|
1139
|
+
for (; this.noteBuffer.length > 0 && this.noteCursorX < e; )
|
|
1140
|
+
this.renderNextNote(), this.noteCursorX += _;
|
|
1141
|
+
this.activeEntries.length > 1 && (this.noteCursorX -= _);
|
|
1142
|
+
}
|
|
1143
|
+
renderNextNote() {
|
|
1144
|
+
if (this.noteBuffer.length < 1) return;
|
|
1145
|
+
const e = this.noteBuffer[0], t = this.svgRendererInstance.createGroup("note-wrapper");
|
|
1146
|
+
if (e.type === "chord") {
|
|
1147
|
+
const s = this.noteRendererInstance.renderChord(e.notes);
|
|
1148
|
+
s.noteGroup.setAttribute("transform", `translate(0, ${s.noteYPos})`), t.appendChild(s.noteGroup);
|
|
1149
|
+
} else {
|
|
1150
|
+
const s = this.noteRendererInstance.renderNote(e.notes[0]);
|
|
1151
|
+
s.noteGroup.setAttribute("transform", `translate(0, ${s.noteYPos})`), t.appendChild(s.noteGroup);
|
|
1152
|
+
}
|
|
1153
|
+
t.style.transform = `translate(${this.noteCursorX}px, 0px)`, this.activeEntries.push({
|
|
1154
|
+
noteWrapper: t,
|
|
1155
|
+
xPos: this.noteCursorX
|
|
1156
|
+
}), this.noteBuffer.shift(), this.notesLayer.appendChild(t);
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Adds notes to the queue for scrolling staff. Clears any previously added notes.
|
|
1160
|
+
* @param notes - A array of note strings or sub-arrays of strings.
|
|
1161
|
+
* * A single string will draw a single note `C#4w`
|
|
1162
|
+
* * A sub-array will draw a chord `["C4w", "E4w", "G4w"]`
|
|
1163
|
+
*
|
|
1164
|
+
* Note string format
|
|
1165
|
+
* * **Root**: (A-G)
|
|
1166
|
+
* * **Accidental** (Optional): `#` (sharp) `b` (flat) `n` (natural) `##` (double sharp) or `bb` (double flat).
|
|
1167
|
+
* * **Octave**: The octave number (e.g., `4`).
|
|
1168
|
+
* * **Duration** (Optional): `w` (whole) `h` (half) `q` (quarter) or `e` (eighth). Defaults to `w` if duration is omitted
|
|
1169
|
+
* @param noteIndex The index of the note that will replaced by the specified note.
|
|
1170
|
+
* @returns void
|
|
1171
|
+
* @throws {Error} If the index provided is out of bounds, or if a note string is not correct format.
|
|
1172
|
+
*
|
|
1173
|
+
* * @example
|
|
1174
|
+
* // Queues a few single notes, followed by a C chord
|
|
1175
|
+
* queueNotes("Bb3q", "C4w", "G4q", ["C4w", "E4w", "G4w"]);
|
|
1176
|
+
*/
|
|
1177
|
+
queueNotes(e) {
|
|
1178
|
+
this.clearAllNotes();
|
|
1179
|
+
for (const t of e)
|
|
1180
|
+
Array.isArray(t) ? this.noteBuffer.push({
|
|
1181
|
+
type: "chord",
|
|
1182
|
+
notes: t
|
|
1183
|
+
}) : this.noteBuffer.push({
|
|
1184
|
+
type: "note",
|
|
1185
|
+
notes: [t]
|
|
1186
|
+
});
|
|
1187
|
+
this.renderFirstNoteGroups();
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Advances to the next note in sequence, if theres any remaining notes left.
|
|
1191
|
+
* @returns void
|
|
1192
|
+
* @callback onNotesOut passed in from the constructor options once notes are out.
|
|
1193
|
+
*/
|
|
1194
|
+
advanceNotes() {
|
|
1195
|
+
if (this.activeEntries.length <= 0) {
|
|
1196
|
+
this.clearAllNotes(), this.options.onNotesOut && this.options.onNotesOut();
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
this.activeEntries.forEach((t) => {
|
|
1200
|
+
t.xPos -= _, t.noteWrapper.style.transform = `translate(${t.xPos}px, 0px)`;
|
|
1201
|
+
});
|
|
1202
|
+
const e = this.activeEntries[0];
|
|
1203
|
+
e.xPos <= 0 && (this.notesLayer.removeChild(e.noteWrapper), this.activeEntries.shift()), this.renderNextNote();
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Clears staff of notes and resets internal positioning.
|
|
1207
|
+
* @returns void
|
|
1208
|
+
*/
|
|
1209
|
+
clearAllNotes() {
|
|
1210
|
+
this.noteCursorX = 0, this.notesLayer.replaceChildren(), this.activeEntries = [], this.noteBuffer = [];
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Removes the root svg element and cleans up arrays.
|
|
1214
|
+
* @returns void
|
|
1215
|
+
*/
|
|
1216
|
+
destroy() {
|
|
1217
|
+
this.svgRendererInstance.destroy(), this.activeEntries = [], this.noteBuffer = [];
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
export {
|
|
1221
|
+
j as MusicStaff,
|
|
1222
|
+
Q as RhythmStaff,
|
|
1223
|
+
z as ScrollingStaff
|
|
1224
|
+
};
|