1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | import { Vex } from './vex';
|
13 | import { Flow } from './tables';
|
14 | import { BoundingBox } from './boundingbox';
|
15 | import { Stem } from './stem';
|
16 | import { NoteHead } from './notehead';
|
17 | import { StemmableNote } from './stemmablenote';
|
18 | import { Modifier } from './modifier';
|
19 | import { Dot } from './dot';
|
20 |
|
21 |
|
22 | function L(...args) { if (StaveNote.DEBUG) Vex.L('Vex.Flow.StaveNote', args); }
|
23 |
|
24 | const getStemAdjustment = (note) => Stem.WIDTH / (2 * -note.getStemDirection());
|
25 |
|
26 | const isInnerNoteIndex = (note, index) =>
|
27 | index === (note.getStemDirection() === Stem.UP ? note.keyProps.length - 1 : 0);
|
28 |
|
29 |
|
30 | function shiftRestVertical(rest, note, dir) {
|
31 | const delta = (note.isrest ? 0.0 : 1.0) * dir;
|
32 |
|
33 | rest.line += delta;
|
34 | rest.maxLine += delta;
|
35 | rest.minLine += delta;
|
36 | rest.note.setKeyLine(0, rest.note.getKeyLine(0) + (delta));
|
37 | }
|
38 |
|
39 |
|
40 | function centerRest(rest, noteU, noteL) {
|
41 | const delta = rest.line - Vex.MidLine(noteU.minLine, noteL.maxLine);
|
42 | rest.note.setKeyLine(0, rest.note.getKeyLine(0) - delta);
|
43 | rest.line -= delta;
|
44 | rest.maxLine -= delta;
|
45 | rest.minLine -= delta;
|
46 | }
|
47 |
|
48 | export class StaveNote extends StemmableNote {
|
49 | static get CATEGORY() { return 'stavenotes'; }
|
50 | static get STEM_UP() { return Stem.UP; }
|
51 | static get STEM_DOWN() { return Stem.DOWN; }
|
52 | static get DEFAULT_LEDGER_LINE_OFFSET() { return 3; }
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | static format(notes, state) {
|
58 | if (!notes || notes.length < 2) return false;
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | if (notes[0].getStave()) {
|
73 | return StaveNote.formatByY(notes, state);
|
74 | }
|
75 |
|
76 | const notesList = [];
|
77 |
|
78 | for (let i = 0; i < notes.length; i++) {
|
79 | const props = notes[i].getKeyProps();
|
80 | const line = props[0].line;
|
81 | let minL = props[props.length - 1].line;
|
82 | const stemDirection = notes[i].getStemDirection();
|
83 | const stemMax = notes[i].getStemLength() / 10;
|
84 | const stemMin = notes[i].getStemMinumumLength() / 10;
|
85 |
|
86 | let maxL;
|
87 | if (notes[i].isRest()) {
|
88 | maxL = line + notes[i].glyph.line_above;
|
89 | minL = line - notes[i].glyph.line_below;
|
90 | } else {
|
91 | maxL = stemDirection === 1
|
92 | ? props[props.length - 1].line + stemMax
|
93 | : props[props.length - 1].line;
|
94 |
|
95 | minL = stemDirection === 1
|
96 | ? props[0].line
|
97 | : props[0].line - stemMax;
|
98 | }
|
99 |
|
100 | notesList.push({
|
101 | line: props[0].line,
|
102 | maxLine: maxL,
|
103 | minLine: minL,
|
104 | isrest: notes[i].isRest(),
|
105 | stemDirection,
|
106 | stemMax,
|
107 | stemMin,
|
108 | voice_shift: notes[i].getVoiceShiftWidth(),
|
109 | is_displaced: notes[i].isDisplaced(),
|
110 | note: notes[i],
|
111 | });
|
112 | }
|
113 |
|
114 | const voices = notesList.length;
|
115 |
|
116 | let noteU = notesList[0];
|
117 | const noteM = voices > 2 ? notesList[1] : null;
|
118 | let noteL = voices > 2 ? notesList[2] : notesList[1];
|
119 |
|
120 |
|
121 |
|
122 | if (voices === 2 && noteU.stemDirection === -1 && noteL.stemDirection === 1) {
|
123 | noteU = notesList[1];
|
124 | noteL = notesList[0];
|
125 | }
|
126 |
|
127 | const voiceXShift = Math.max(noteU.voice_shift, noteL.voice_shift);
|
128 | let xShift = 0;
|
129 | let stemDelta;
|
130 |
|
131 |
|
132 | if (voices === 2) {
|
133 | const lineSpacing = noteU.stemDirection === noteL.stemDirection ? 0.0 : 0.5;
|
134 |
|
135 | if (noteU.stemDirection === noteL.stemDirection &&
|
136 | noteU.minLine <= noteL.maxLine) {
|
137 | if (!noteU.isrest) {
|
138 | stemDelta = Math.abs(noteU.line - (noteL.maxLine + 0.5));
|
139 | stemDelta = Math.max(stemDelta, noteU.stemMin);
|
140 | noteU.minLine = noteU.line - stemDelta;
|
141 | noteU.note.setStemLength(stemDelta * 10);
|
142 | }
|
143 | }
|
144 | if (noteU.minLine <= noteL.maxLine + lineSpacing) {
|
145 | if (noteU.isrest) {
|
146 |
|
147 | shiftRestVertical(noteU, noteL, 1);
|
148 | } else if (noteL.isrest) {
|
149 |
|
150 | shiftRestVertical(noteL, noteU, -1);
|
151 | } else {
|
152 | xShift = voiceXShift;
|
153 | if (noteU.stemDirection === noteL.stemDirection) {
|
154 |
|
155 | noteU.note.setXShift(xShift + 3);
|
156 | } else {
|
157 |
|
158 | noteL.note.setXShift(xShift);
|
159 | }
|
160 | }
|
161 | }
|
162 |
|
163 |
|
164 | return true;
|
165 | }
|
166 |
|
167 |
|
168 | if (noteM !== null && noteM.minLine < noteL.maxLine + 0.5) {
|
169 | if (!noteM.isrest) {
|
170 | stemDelta = Math.abs(noteM.line - (noteL.maxLine + 0.5));
|
171 | stemDelta = Math.max(stemDelta, noteM.stemMin);
|
172 | noteM.minLine = noteM.line - stemDelta;
|
173 | noteM.note.setStemLength(stemDelta * 10);
|
174 | }
|
175 | }
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 | if (noteM.isrest && !noteU.isrest && !noteL.isrest) {
|
182 | if (noteU.minLine <= noteM.maxLine || noteM.minLine <= noteL.maxLine) {
|
183 | const restHeight = noteM.maxLine - noteM.minLine;
|
184 | const space = noteU.minLine - noteL.maxLine;
|
185 | if (restHeight < space) {
|
186 |
|
187 | centerRest(noteM, noteU, noteL);
|
188 | } else {
|
189 | xShift = voiceXShift + 3;
|
190 | noteM.note.setXShift(xShift);
|
191 | }
|
192 |
|
193 | return true;
|
194 | }
|
195 | }
|
196 |
|
197 |
|
198 | if (noteU.isrest && noteM.isrest && noteL.isrest) {
|
199 |
|
200 | shiftRestVertical(noteU, noteM, 1);
|
201 |
|
202 | shiftRestVertical(noteL, noteM, -1);
|
203 |
|
204 | return true;
|
205 | }
|
206 |
|
207 |
|
208 | if (noteM.isrest && noteU.isrest && noteM.minLine <= noteL.maxLine) {
|
209 |
|
210 | shiftRestVertical(noteM, noteL, 1);
|
211 | }
|
212 | if (noteM.isrest && noteL.isrest && noteU.minLine <= noteM.maxLine) {
|
213 |
|
214 | shiftRestVertical(noteM, noteU, -1);
|
215 | }
|
216 | if (noteU.isrest && noteU.minLine <= noteM.maxLine) {
|
217 |
|
218 | shiftRestVertical(noteU, noteM, 1);
|
219 | }
|
220 | if (noteL.isrest && noteM.minLine <= noteL.maxLine) {
|
221 |
|
222 | shiftRestVertical(noteL, noteM, -1);
|
223 | }
|
224 |
|
225 |
|
226 | if ((!noteU.isrest && !noteM.isrest && noteU.minLine <= noteM.maxLine + 0.5) ||
|
227 | (!noteM.isrest && !noteL.isrest && noteM.minLine <= noteL.maxLine)) {
|
228 | xShift = voiceXShift + 3;
|
229 | noteM.note.setXShift(xShift);
|
230 | }
|
231 |
|
232 | return true;
|
233 | }
|
234 |
|
235 | static formatByY(notes, state) {
|
236 |
|
237 |
|
238 | let hasStave = true;
|
239 |
|
240 | for (let i = 0; i < notes.length; i++) {
|
241 | hasStave = hasStave && notes[i].getStave() != null;
|
242 | }
|
243 |
|
244 | if (!hasStave) {
|
245 | throw new Vex.RERR(
|
246 | 'Stave Missing',
|
247 | 'All notes must have a stave - Vex.Flow.ModifierContext.formatMultiVoice!'
|
248 | );
|
249 | }
|
250 |
|
251 | let xShift = 0;
|
252 |
|
253 | for (let i = 0; i < notes.length - 1; i++) {
|
254 | let topNote = notes[i];
|
255 | let bottomNote = notes[i + 1];
|
256 |
|
257 | if (topNote.getStemDirection() === Stem.DOWN) {
|
258 | topNote = notes[i + 1];
|
259 | bottomNote = notes[i];
|
260 | }
|
261 |
|
262 | const topKeys = topNote.getKeyProps();
|
263 | const bottomKeys = bottomNote.getKeyProps();
|
264 |
|
265 | const HALF_NOTEHEAD_HEIGHT = 0.5;
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 | const topNoteBottomY = topNote
|
274 | .getStave()
|
275 | .getYForLine(5 - topKeys[0].line + HALF_NOTEHEAD_HEIGHT);
|
276 |
|
277 | const bottomNoteTopY = bottomNote
|
278 | .getStave()
|
279 | .getYForLine(5 - bottomKeys[bottomKeys.length - 1].line - HALF_NOTEHEAD_HEIGHT);
|
280 |
|
281 | const areNotesColliding = bottomNoteTopY - topNoteBottomY < 0;
|
282 |
|
283 | if (areNotesColliding) {
|
284 | xShift = topNote.getVoiceShiftWidth() + 2;
|
285 | bottomNote.setXShift(xShift);
|
286 | }
|
287 | }
|
288 |
|
289 | state.right_shift += xShift;
|
290 | }
|
291 |
|
292 | static postFormat(notes) {
|
293 | if (!notes) return false;
|
294 |
|
295 | notes.forEach(note => note.postFormat());
|
296 |
|
297 | return true;
|
298 | }
|
299 |
|
300 | constructor(noteStruct) {
|
301 | super(noteStruct);
|
302 | this.setAttribute('type', 'StaveNote');
|
303 |
|
304 | this.keys = noteStruct.keys;
|
305 | this.clef = noteStruct.clef;
|
306 | this.octave_shift = noteStruct.octave_shift;
|
307 | this.beam = null;
|
308 |
|
309 |
|
310 | this.glyph = Flow.getGlyphProps(this.duration, this.noteType);
|
311 |
|
312 | if (!this.glyph) {
|
313 | throw new Vex.RuntimeError(
|
314 | 'BadArguments',
|
315 | `Invalid note initialization data (No glyph found): ${JSON.stringify(noteStruct)}`
|
316 | );
|
317 | }
|
318 |
|
319 |
|
320 | this.displaced = false;
|
321 | this.dot_shiftY = 0;
|
322 |
|
323 | this.keyProps = [];
|
324 |
|
325 | this.use_default_head_x = false;
|
326 |
|
327 |
|
328 | this.note_heads = [];
|
329 | this.modifiers = [];
|
330 |
|
331 | Vex.Merge(this.render_options, {
|
332 |
|
333 | glyph_font_scale: noteStruct.glyph_font_scale || Flow.DEFAULT_NOTATION_FONT_SCALE,
|
334 |
|
335 | stroke_px: noteStruct.stroke_px || StaveNote.DEFAULT_LEDGER_LINE_OFFSET,
|
336 | });
|
337 |
|
338 | this.calculateKeyProps();
|
339 | this.buildStem();
|
340 |
|
341 |
|
342 | if (noteStruct.auto_stem) {
|
343 | this.autoStem();
|
344 | } else {
|
345 | this.setStemDirection(noteStruct.stem_direction);
|
346 | }
|
347 | this.reset();
|
348 | this.buildFlag();
|
349 | }
|
350 |
|
351 | reset() {
|
352 | super.reset();
|
353 |
|
354 |
|
355 | const noteHeadStyles = this.note_heads.map(noteHead => noteHead.getStyle());
|
356 | this.buildNoteHeads();
|
357 | this.note_heads.forEach((noteHead, index) => noteHead.setStyle(noteHeadStyles[index]));
|
358 |
|
359 | if (this.stave) {
|
360 | this.note_heads.forEach(head => head.setStave(this.stave));
|
361 | }
|
362 | this.calcNoteDisplacements();
|
363 | }
|
364 |
|
365 | setBeam(beam) {
|
366 | this.beam = beam;
|
367 | this.calcNoteDisplacements();
|
368 | return this;
|
369 | }
|
370 |
|
371 | getCategory() { return StaveNote.CATEGORY; }
|
372 |
|
373 |
|
374 | buildStem() {
|
375 | this.setStem(new Stem({ hide: !!this.isRest(), }));
|
376 | }
|
377 |
|
378 |
|
379 | buildNoteHeads() {
|
380 | this.note_heads = [];
|
381 | const stemDirection = this.getStemDirection();
|
382 | const keys = this.getKeys();
|
383 |
|
384 | let lastLine = null;
|
385 | let lineDiff = null;
|
386 | let displaced = false;
|
387 |
|
388 |
|
389 |
|
390 |
|
391 | let start;
|
392 | let end;
|
393 | let step;
|
394 | if (stemDirection === Stem.UP) {
|
395 | start = 0;
|
396 | end = keys.length;
|
397 | step = 1;
|
398 | } else if (stemDirection === Stem.DOWN) {
|
399 | start = keys.length - 1;
|
400 | end = -1;
|
401 | step = -1;
|
402 | }
|
403 |
|
404 | for (let i = start; i !== end; i += step) {
|
405 | const noteProps = this.keyProps[i];
|
406 | const line = noteProps.line;
|
407 |
|
408 |
|
409 |
|
410 | if (lastLine === null) {
|
411 | lastLine = line;
|
412 | } else {
|
413 | lineDiff = Math.abs(lastLine - line);
|
414 | if (lineDiff === 0 || lineDiff === 0.5) {
|
415 | displaced = !displaced;
|
416 | } else {
|
417 | displaced = false;
|
418 | this.use_default_head_x = true;
|
419 | }
|
420 | }
|
421 | lastLine = line;
|
422 |
|
423 | const notehead = new NoteHead({
|
424 | duration: this.duration,
|
425 | note_type: this.noteType,
|
426 | displaced,
|
427 | stem_direction: stemDirection,
|
428 | custom_glyph_code: noteProps.code,
|
429 | glyph_font_scale: this.render_options.glyph_font_scale,
|
430 | x_shift: noteProps.shift_right,
|
431 | stem_up_x_offset: noteProps.stem_up_x_offset,
|
432 | stem_down_x_offset: noteProps.stem_down_x_offset,
|
433 | line: noteProps.line,
|
434 | });
|
435 |
|
436 | this.note_heads[i] = notehead;
|
437 | }
|
438 | }
|
439 |
|
440 |
|
441 | autoStem() {
|
442 |
|
443 | this.minLine = this.keyProps[0].line;
|
444 | this.maxLine = this.keyProps[this.keyProps.length - 1].line;
|
445 |
|
446 | const MIDDLE_LINE = 3;
|
447 | const decider = (this.minLine + this.maxLine) / 2;
|
448 | const stemDirection = decider < MIDDLE_LINE ? Stem.UP : Stem.DOWN;
|
449 |
|
450 | this.setStemDirection(stemDirection);
|
451 | }
|
452 |
|
453 |
|
454 | calculateKeyProps() {
|
455 | let lastLine = null;
|
456 | for (let i = 0; i < this.keys.length; ++i) {
|
457 | const key = this.keys[i];
|
458 |
|
459 |
|
460 |
|
461 | if (this.glyph.rest) this.glyph.position = key;
|
462 |
|
463 | const options = { octave_shift: this.octave_shift || 0 };
|
464 | const props = Flow.keyProperties(key, this.clef, options);
|
465 |
|
466 | if (!props) {
|
467 | throw new Vex.RuntimeError('BadArguments', `Invalid key for note properties: ${key}`);
|
468 | }
|
469 |
|
470 |
|
471 | if (props.key === 'R') {
|
472 | if (this.duration === '1' || this.duration === 'w') {
|
473 | props.line = 4;
|
474 | } else {
|
475 | props.line = 3;
|
476 | }
|
477 | }
|
478 |
|
479 |
|
480 | const line = props.line;
|
481 | if (lastLine === null) {
|
482 | lastLine = line;
|
483 | } else {
|
484 | if (Math.abs(lastLine - line) === 0.5) {
|
485 | this.displaced = true;
|
486 | props.displaced = true;
|
487 |
|
488 |
|
489 |
|
490 | if (this.keyProps.length > 0) {
|
491 | this.keyProps[i - 1].displaced = true;
|
492 | }
|
493 | }
|
494 | }
|
495 |
|
496 | lastLine = line;
|
497 | this.keyProps.push(props);
|
498 | }
|
499 |
|
500 |
|
501 | lastLine = -Infinity;
|
502 | this.keyProps.forEach(key => {
|
503 | if (key.line < lastLine) {
|
504 | Vex.W(
|
505 | 'Unsorted keys in note will be sorted. ' +
|
506 | 'See https://github.com/0xfe/vexflow/issues/104 for details.'
|
507 | );
|
508 | }
|
509 | lastLine = key.line;
|
510 | });
|
511 | this.keyProps.sort((a, b) => a.line - b.line);
|
512 | }
|
513 |
|
514 |
|
515 | getBoundingBox() {
|
516 | if (!this.preFormatted) {
|
517 | throw new Vex.RERR('UnformattedNote', "Can't call getBoundingBox on an unformatted note.");
|
518 | }
|
519 |
|
520 | const { width: w, modLeftPx, leftDisplacedHeadPx } = this.getMetrics();
|
521 | const x = this.getAbsoluteX() - modLeftPx - leftDisplacedHeadPx;
|
522 |
|
523 | let minY = 0;
|
524 | let maxY = 0;
|
525 | const halfLineSpacing = this.getStave().getSpacingBetweenLines() / 2;
|
526 | const lineSpacing = halfLineSpacing * 2;
|
527 |
|
528 | if (this.isRest()) {
|
529 | const y = this.ys[0];
|
530 | const frac = Flow.durationToFraction(this.duration);
|
531 | if (frac.equals(1) || frac.equals(2)) {
|
532 | minY = y - halfLineSpacing;
|
533 | maxY = y + halfLineSpacing;
|
534 | } else {
|
535 | minY = y - (this.glyph.line_above * lineSpacing);
|
536 | maxY = y + (this.glyph.line_below * lineSpacing);
|
537 | }
|
538 | } else if (this.glyph.stem) {
|
539 | const ys = this.getStemExtents();
|
540 | ys.baseY += halfLineSpacing * this.stem_direction;
|
541 | minY = Math.min(ys.topY, ys.baseY);
|
542 | maxY = Math.max(ys.topY, ys.baseY);
|
543 | } else {
|
544 | minY = null;
|
545 | maxY = null;
|
546 |
|
547 | for (let i = 0; i < this.ys.length; ++i) {
|
548 | const yy = this.ys[i];
|
549 | if (i === 0) {
|
550 | minY = yy;
|
551 | maxY = yy;
|
552 | } else {
|
553 | minY = Math.min(yy, minY);
|
554 | maxY = Math.max(yy, maxY);
|
555 | }
|
556 | }
|
557 | minY -= halfLineSpacing;
|
558 | maxY += halfLineSpacing;
|
559 | }
|
560 |
|
561 | return new BoundingBox(x, minY, w, maxY - minY);
|
562 | }
|
563 |
|
564 |
|
565 |
|
566 | getLineNumber(isTopNote) {
|
567 | if (!this.keyProps.length) {
|
568 | throw new Vex.RERR(
|
569 | 'NoKeyProps', "Can't get bottom note line, because note is not initialized properly."
|
570 | );
|
571 | }
|
572 |
|
573 | let resultLine = this.keyProps[0].line;
|
574 |
|
575 |
|
576 | for (let i = 0; i < this.keyProps.length; i++) {
|
577 | const thisLine = this.keyProps[i].line;
|
578 | if (isTopNote) {
|
579 | if (thisLine > resultLine) resultLine = thisLine;
|
580 | } else {
|
581 | if (thisLine < resultLine) resultLine = thisLine;
|
582 | }
|
583 | }
|
584 |
|
585 | return resultLine;
|
586 | }
|
587 |
|
588 |
|
589 | isRest() { return this.glyph.rest; }
|
590 |
|
591 |
|
592 | isChord() { return !this.isRest() && this.keys.length > 1; }
|
593 |
|
594 |
|
595 | hasStem() { return this.glyph.stem; }
|
596 |
|
597 | hasFlag() {
|
598 | return super.hasFlag() && !this.isRest();
|
599 | }
|
600 |
|
601 | getStemX() {
|
602 | if (this.noteType === 'r') {
|
603 | return this.getCenterGlyphX();
|
604 | } else {
|
605 |
|
606 |
|
607 | return super.getStemX() + getStemAdjustment(this);
|
608 | }
|
609 | }
|
610 |
|
611 |
|
612 |
|
613 | getYForTopText(textLine) {
|
614 | const extents = this.getStemExtents();
|
615 | return Math.min(
|
616 | this.stave.getYForTopText(textLine),
|
617 | extents.topY - (this.render_options.annotation_spacing * (textLine + 1))
|
618 | );
|
619 | }
|
620 | getYForBottomText(textLine) {
|
621 | const extents = this.getStemExtents();
|
622 | return Math.max(
|
623 | this.stave.getYForTopText(textLine),
|
624 | extents.baseY + (this.render_options.annotation_spacing * (textLine))
|
625 | );
|
626 | }
|
627 |
|
628 |
|
629 |
|
630 | setStave(stave) {
|
631 | super.setStave(stave);
|
632 |
|
633 | const ys = this.note_heads.map(notehead => {
|
634 | notehead.setStave(stave);
|
635 | return notehead.getY();
|
636 | });
|
637 |
|
638 | this.setYs(ys);
|
639 |
|
640 | if (this.stem) {
|
641 | const { y_top, y_bottom } = this.getNoteHeadBounds();
|
642 | this.stem.setYBounds(y_top, y_bottom);
|
643 | }
|
644 |
|
645 | return this;
|
646 | }
|
647 |
|
648 |
|
649 | getKeys() { return this.keys; }
|
650 |
|
651 |
|
652 | getKeyProps() {
|
653 | return this.keyProps;
|
654 | }
|
655 |
|
656 |
|
657 | isDisplaced() {
|
658 | return this.displaced;
|
659 | }
|
660 |
|
661 |
|
662 | setNoteDisplaced(displaced) {
|
663 | this.displaced = displaced;
|
664 | return this;
|
665 | }
|
666 |
|
667 |
|
668 | getTieRightX() {
|
669 | let tieStartX = this.getAbsoluteX();
|
670 | tieStartX += this.getGlyphWidth() + this.x_shift + this.rightDisplacedHeadPx;
|
671 | if (this.modifierContext) tieStartX += this.modifierContext.getRightShift();
|
672 | return tieStartX;
|
673 | }
|
674 |
|
675 |
|
676 | getTieLeftX() {
|
677 | let tieEndX = this.getAbsoluteX();
|
678 | tieEndX += this.x_shift - this.leftDisplacedHeadPx;
|
679 | return tieEndX;
|
680 | }
|
681 |
|
682 |
|
683 | getLineForRest() {
|
684 | let restLine = this.keyProps[0].line;
|
685 | if (this.keyProps.length > 1) {
|
686 | const lastLine = this.keyProps[this.keyProps.length - 1].line;
|
687 | const top = Math.max(restLine, lastLine);
|
688 | const bot = Math.min(restLine, lastLine);
|
689 | restLine = Vex.MidLine(top, bot);
|
690 | }
|
691 |
|
692 | return restLine;
|
693 | }
|
694 |
|
695 |
|
696 |
|
697 | getModifierStartXY(position, index, options) {
|
698 | options = options || {};
|
699 | if (!this.preFormatted) {
|
700 | throw new Vex.RERR('UnformattedNote', "Can't call GetModifierStartXY on an unformatted note");
|
701 | }
|
702 |
|
703 | if (this.ys.length === 0) {
|
704 | throw new Vex.RERR('NoYValues', 'No Y-Values calculated for this note.');
|
705 | }
|
706 |
|
707 | const { ABOVE, BELOW, LEFT, RIGHT } = Modifier.Position;
|
708 | let x = 0;
|
709 | if (position === LEFT) {
|
710 |
|
711 | x = -1 * 2;
|
712 | } else if (position === RIGHT) {
|
713 |
|
714 | x = this.getGlyphWidth() + this.x_shift + 2;
|
715 |
|
716 | if (this.stem_direction === Stem.UP && this.hasFlag() &&
|
717 | (options.forceFlagRight || isInnerNoteIndex(this, index))) {
|
718 | x += this.flag.getMetrics().width;
|
719 | }
|
720 | } else if (position === BELOW || position === ABOVE) {
|
721 | x = this.getGlyphWidth() / 2;
|
722 | }
|
723 |
|
724 | return {
|
725 | x: this.getAbsoluteX() + x,
|
726 | y: this.ys[index],
|
727 | };
|
728 | }
|
729 |
|
730 |
|
731 |
|
732 | setStyle(style) {
|
733 | super.setStyle(style);
|
734 | this.note_heads.forEach(notehead => notehead.setStyle(style));
|
735 | this.stem.setStyle(style);
|
736 | }
|
737 |
|
738 | setStemStyle(style) {
|
739 | const stem = this.getStem();
|
740 | stem.setStyle(style);
|
741 | }
|
742 | getStemStyle() { return this.stem.getStyle(); }
|
743 |
|
744 | setLedgerLineStyle(style) { this.ledgerLineStyle = style; }
|
745 | getLedgerLineStyle() { return this.ledgerLineStyle; }
|
746 |
|
747 | setFlagStyle(style) { this.flagStyle = style; }
|
748 | getFlagStyle() { return this.flagStyle; }
|
749 |
|
750 |
|
751 |
|
752 |
|
753 |
|
754 | setKeyStyle(index, style) {
|
755 | this.note_heads[index].setStyle(style);
|
756 | return this;
|
757 | }
|
758 |
|
759 | setKeyLine(index, line) {
|
760 | this.keyProps[index].line = line;
|
761 | this.reset();
|
762 | return this;
|
763 | }
|
764 |
|
765 | getKeyLine(index) {
|
766 | return this.keyProps[index].line;
|
767 | }
|
768 |
|
769 |
|
770 |
|
771 | addToModifierContext(mContext) {
|
772 | this.setModifierContext(mContext);
|
773 | for (let i = 0; i < this.modifiers.length; ++i) {
|
774 | this.modifierContext.addModifier(this.modifiers[i]);
|
775 | }
|
776 | this.modifierContext.addModifier(this);
|
777 | this.setPreFormatted(false);
|
778 | return this;
|
779 | }
|
780 |
|
781 |
|
782 |
|
783 |
|
784 |
|
785 |
|
786 | addModifier(index, modifier) {
|
787 | modifier.setNote(this);
|
788 | modifier.setIndex(index);
|
789 | this.modifiers.push(modifier);
|
790 | this.setPreFormatted(false);
|
791 | return this;
|
792 | }
|
793 |
|
794 |
|
795 | addAccidental(index, accidental) {
|
796 | return this.addModifier(index, accidental);
|
797 | }
|
798 |
|
799 |
|
800 | addArticulation(index, articulation) {
|
801 | return this.addModifier(index, articulation);
|
802 | }
|
803 |
|
804 |
|
805 | addAnnotation(index, annotation) {
|
806 | return this.addModifier(index, annotation);
|
807 | }
|
808 |
|
809 |
|
810 | addDot(index) {
|
811 | const dot = new Dot();
|
812 | dot.setDotShiftY(this.glyph.dot_shiftY);
|
813 | this.dots++;
|
814 | return this.addModifier(index, dot);
|
815 | }
|
816 |
|
817 |
|
818 | addDotToAll() {
|
819 | for (let i = 0; i < this.keys.length; ++i) {
|
820 | this.addDot(i);
|
821 | }
|
822 | return this;
|
823 | }
|
824 |
|
825 |
|
826 | getAccidentals() {
|
827 | return this.modifierContext.getModifiers('accidentals');
|
828 | }
|
829 |
|
830 |
|
831 | getDots() {
|
832 | return this.modifierContext.getModifiers('dots');
|
833 | }
|
834 |
|
835 |
|
836 |
|
837 | getVoiceShiftWidth() {
|
838 |
|
839 | return this.getGlyphWidth() * (this.displaced ? 2 : 1);
|
840 | }
|
841 |
|
842 |
|
843 |
|
844 | calcNoteDisplacements() {
|
845 | this.setLeftDisplacedHeadPx(
|
846 | this.displaced && this.stem_direction === Stem.DOWN
|
847 | ? this.getGlyphWidth()
|
848 | : 0
|
849 | );
|
850 |
|
851 |
|
852 |
|
853 | this.setRightDisplacedHeadPx(
|
854 | !this.hasFlag() && this.displaced && this.stem_direction === Stem.UP
|
855 | ? this.getGlyphWidth()
|
856 | : 0
|
857 | );
|
858 | }
|
859 |
|
860 |
|
861 | preFormat() {
|
862 | if (this.preFormatted) return;
|
863 | if (this.modifierContext) this.modifierContext.preFormat();
|
864 |
|
865 | let width = this.getGlyphWidth() + this.leftDisplacedHeadPx + this.rightDisplacedHeadPx;
|
866 |
|
867 |
|
868 | if (this.glyph.flag && this.beam === null && this.stem_direction === Stem.UP) {
|
869 | width += this.getGlyphWidth();
|
870 |
|
871 | }
|
872 |
|
873 | this.setWidth(width);
|
874 | this.setPreFormatted(true);
|
875 | }
|
876 |
|
877 | |
878 |
|
879 |
|
880 |
|
881 |
|
882 |
|
883 |
|
884 |
|
885 |
|
886 |
|
887 |
|
888 |
|
889 |
|
890 |
|
891 |
|
892 |
|
893 | |
894 |
|
895 |
|
896 |
|
897 | getNoteHeadBounds() {
|
898 |
|
899 | let yTop = null;
|
900 | let yBottom = null;
|
901 | let nonDisplacedX = null;
|
902 | let displacedX = null;
|
903 |
|
904 | let highestLine = this.stave.getNumLines();
|
905 | let lowestLine = 1;
|
906 | let highestDisplacedLine = false;
|
907 | let lowestDisplacedLine = false;
|
908 | let highestNonDisplacedLine = highestLine;
|
909 | let lowestNonDisplacedLine = lowestLine;
|
910 |
|
911 | this.note_heads.forEach(notehead => {
|
912 | const line = notehead.getLine();
|
913 | const y = notehead.getY();
|
914 |
|
915 | if (yTop === null || y < yTop) {
|
916 | yTop = y;
|
917 | }
|
918 |
|
919 | if (yBottom === null || y > yBottom) {
|
920 | yBottom = y;
|
921 | }
|
922 |
|
923 | if (displacedX === null && notehead.isDisplaced()) {
|
924 | displacedX = notehead.getAbsoluteX();
|
925 | }
|
926 |
|
927 | if (nonDisplacedX === null && !notehead.isDisplaced()) {
|
928 | nonDisplacedX = notehead.getAbsoluteX();
|
929 | }
|
930 |
|
931 | highestLine = line > highestLine ? line : highestLine;
|
932 | lowestLine = line < lowestLine ? line : lowestLine;
|
933 |
|
934 | if (notehead.isDisplaced()) {
|
935 | highestDisplacedLine = (highestDisplacedLine === false) ?
|
936 | line : Math.max(line, highestDisplacedLine);
|
937 | lowestDisplacedLine = (lowestDisplacedLine === false) ?
|
938 | line : Math.min(line, lowestDisplacedLine);
|
939 | } else {
|
940 | highestNonDisplacedLine = Math.max(line, highestNonDisplacedLine);
|
941 | lowestNonDisplacedLine = Math.min(line, lowestNonDisplacedLine);
|
942 | }
|
943 | }, this);
|
944 |
|
945 | return {
|
946 | y_top: yTop,
|
947 | y_bottom: yBottom,
|
948 | displaced_x: displacedX,
|
949 | non_displaced_x: nonDisplacedX,
|
950 | highest_line: highestLine,
|
951 | lowest_line: lowestLine,
|
952 | highest_displaced_line: highestDisplacedLine,
|
953 | lowest_displaced_line: lowestDisplacedLine,
|
954 | highest_non_displaced_line: highestNonDisplacedLine,
|
955 | lowest_non_displaced_line: lowestNonDisplacedLine,
|
956 | };
|
957 | }
|
958 |
|
959 |
|
960 | getNoteHeadBeginX() {
|
961 | return this.getAbsoluteX() + this.x_shift;
|
962 | }
|
963 |
|
964 |
|
965 | getNoteHeadEndX() {
|
966 | const xBegin = this.getNoteHeadBeginX();
|
967 | return xBegin + this.getGlyphWidth();
|
968 | }
|
969 |
|
970 |
|
971 | drawLedgerLines() {
|
972 | const {
|
973 | stave, glyph,
|
974 | render_options: { stroke_px },
|
975 | context: ctx,
|
976 | } = this;
|
977 |
|
978 | const width = glyph.getWidth() + (stroke_px * 2);
|
979 | const doubleWidth = 2 * (glyph.getWidth() + stroke_px) - (Stem.WIDTH / 2);
|
980 |
|
981 | if (this.isRest()) return;
|
982 | if (!ctx) {
|
983 | throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
|
984 | }
|
985 |
|
986 | const {
|
987 | highest_line,
|
988 | lowest_line,
|
989 | highest_displaced_line,
|
990 | highest_non_displaced_line,
|
991 | lowest_displaced_line,
|
992 | lowest_non_displaced_line,
|
993 | displaced_x,
|
994 | non_displaced_x,
|
995 | } = this.getNoteHeadBounds();
|
996 |
|
997 | const min_x = Math.min(displaced_x, non_displaced_x);
|
998 |
|
999 | const drawLedgerLine = (y, normal, displaced) => {
|
1000 | let x;
|
1001 | if (displaced && normal) x = min_x - stroke_px;
|
1002 | else if (normal) x = non_displaced_x - stroke_px;
|
1003 | else x = displaced_x - stroke_px;
|
1004 | const ledgerWidth = (normal && displaced) ? doubleWidth : width;
|
1005 |
|
1006 | ctx.beginPath();
|
1007 | ctx.moveTo(x, y);
|
1008 | ctx.lineTo(x + ledgerWidth, y);
|
1009 | ctx.stroke();
|
1010 | };
|
1011 |
|
1012 | const style = { ...stave.getStyle() || {}, ...this.getLedgerLineStyle() || {} };
|
1013 | this.applyStyle(ctx, style);
|
1014 |
|
1015 |
|
1016 | for (let line = 6; line <= highest_line; ++line) {
|
1017 | const normal = (non_displaced_x !== null) && (line <= highest_non_displaced_line);
|
1018 | const displaced = (displaced_x !== null) && (line <= highest_displaced_line);
|
1019 | drawLedgerLine(stave.getYForNote(line), normal, displaced);
|
1020 | }
|
1021 |
|
1022 |
|
1023 | for (let line = 0; line >= lowest_line; --line) {
|
1024 | const normal = (non_displaced_x !== null) && (line >= lowest_non_displaced_line);
|
1025 | const displaced = (displaced_x !== null) && (line >= lowest_displaced_line);
|
1026 | drawLedgerLine(stave.getYForNote(line), normal, displaced);
|
1027 | }
|
1028 |
|
1029 | this.restoreStyle(ctx, style);
|
1030 | }
|
1031 |
|
1032 |
|
1033 | drawModifiers() {
|
1034 | if (!this.context) {
|
1035 | throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
|
1036 | }
|
1037 |
|
1038 | const ctx = this.context;
|
1039 | ctx.openGroup('modifiers');
|
1040 | for (let i = 0; i < this.modifiers.length; i++) {
|
1041 | const modifier = this.modifiers[i];
|
1042 | const notehead = this.note_heads[modifier.getIndex()];
|
1043 | const noteheadStyle = notehead.getStyle();
|
1044 | notehead.applyStyle(ctx, noteheadStyle);
|
1045 | modifier.setContext(ctx);
|
1046 | modifier.drawWithStyle();
|
1047 | notehead.restoreStyle(ctx, noteheadStyle);
|
1048 | }
|
1049 | ctx.closeGroup();
|
1050 | }
|
1051 |
|
1052 |
|
1053 | drawFlag() {
|
1054 | const { stem, beam, context: ctx } = this;
|
1055 |
|
1056 | if (!ctx) {
|
1057 | throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
|
1058 | }
|
1059 |
|
1060 | const shouldRenderFlag = beam === null;
|
1061 | const glyph = this.getGlyph();
|
1062 |
|
1063 | if (glyph.flag && shouldRenderFlag) {
|
1064 | const { y_top, y_bottom } = this.getNoteHeadBounds();
|
1065 | const noteStemHeight = stem.getHeight();
|
1066 | const flagX = this.getStemX();
|
1067 |
|
1068 | const flagY = this.getStemDirection() === Stem.DOWN
|
1069 |
|
1070 | ? y_top - noteStemHeight + 2
|
1071 |
|
1072 | : y_bottom - noteStemHeight - 2;
|
1073 |
|
1074 |
|
1075 | ctx.openGroup('flag', null, { pointerBBox: true });
|
1076 | this.applyStyle(ctx, this.getFlagStyle() || false);
|
1077 | this.flag.render(ctx, flagX, flagY);
|
1078 | this.restoreStyle(ctx, this.getFlagStyle() || false);
|
1079 | ctx.closeGroup();
|
1080 | }
|
1081 | }
|
1082 |
|
1083 |
|
1084 | drawNoteHeads() {
|
1085 | this.note_heads.forEach(notehead => {
|
1086 | this.context.openGroup('notehead', null, { pointerBBox: true });
|
1087 | notehead.setContext(this.context).draw();
|
1088 | this.context.closeGroup();
|
1089 | });
|
1090 | }
|
1091 |
|
1092 | drawStem(stemStruct) {
|
1093 |
|
1094 |
|
1095 |
|
1096 | if (!this.context) {
|
1097 | throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
|
1098 | }
|
1099 |
|
1100 | if (stemStruct) {
|
1101 | this.setStem(new Stem(stemStruct));
|
1102 | }
|
1103 |
|
1104 | this.context.openGroup('stem', null, { pointerBBox: true });
|
1105 | this.stem.setContext(this.context).draw();
|
1106 | this.context.closeGroup();
|
1107 | }
|
1108 |
|
1109 |
|
1110 | draw() {
|
1111 | if (!this.context) {
|
1112 | throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
|
1113 | }
|
1114 | if (!this.stave) {
|
1115 | throw new Vex.RERR('NoStave', "Can't draw without a stave.");
|
1116 | }
|
1117 | if (this.ys.length === 0) {
|
1118 | throw new Vex.RERR('NoYValues', "Can't draw note without Y values.");
|
1119 | }
|
1120 |
|
1121 | const xBegin = this.getNoteHeadBeginX();
|
1122 | const shouldRenderStem = this.hasStem() && !this.beam;
|
1123 |
|
1124 |
|
1125 | this.note_heads.forEach(notehead => notehead.setX(xBegin));
|
1126 |
|
1127 |
|
1128 | const stemX = this.getStemX();
|
1129 | this.stem.setNoteHeadXBounds(stemX, stemX);
|
1130 |
|
1131 | L('Rendering ', this.isChord() ? 'chord :' : 'note :', this.keys);
|
1132 |
|
1133 |
|
1134 | this.drawLedgerLines();
|
1135 |
|
1136 |
|
1137 | this.applyStyle();
|
1138 | this.setAttribute('el', this.context.openGroup('stavenote', this.getAttribute('id')));
|
1139 | this.context.openGroup('note', null, { pointerBBox: true });
|
1140 | if (shouldRenderStem) this.drawStem();
|
1141 | this.drawNoteHeads();
|
1142 | this.drawFlag();
|
1143 | this.context.closeGroup();
|
1144 | this.drawModifiers();
|
1145 | this.context.closeGroup();
|
1146 | this.restoreStyle();
|
1147 | this.setRendered();
|
1148 | }
|
1149 | }
|